Skip to content

feat: add maybe and result thread macros#1403

Open
scolsen wants to merge 1 commit intocarp-lang:masterfrom
scolsen:maybe-thread
Open

feat: add maybe and result thread macros#1403
scolsen wants to merge 1 commit intocarp-lang:masterfrom
scolsen:maybe-thread

Conversation

@scolsen
Copy link
Copy Markdown
Contributor

@scolsen scolsen commented Mar 24, 2022

These macros each thread the value of a Maybe or Result through
subsequent forms only when the Maybe or Result is a Just/Success value,
returning Nothing/Error otherwise.

It's a convenient way to execute some sequence of forms when a
Maybe/Result returning function is successful, immediately returning the
error value otherwise.

Example:

(defn foo [x] (?-> x (+ 2) (+ 4)))
(foo (Maybe.Just 2)) ;=> (Maybe.Just 6)
(foo (Maybe.Nothing)) ;=> (Maybe.Nothing)
(defn foo [x] (/-> x (+ 2) (+ 4)))
(foo (Result.Success 2)) ;=> (Result.Success 6) ;; you'd actually need a `the` to type check the RESULT in real code 
(foo (Result.Error @"err")) ;=> (Result.Error @"err")

These macros each thread the value of a Maybe or Result through
subsequent forms only when the Maybe or Result is a Just/Success value,
returning Nothing/Error otherwise.

It's a convenient way to execute some sequence of forms when a
Maybe/Result returning function is successful, immediately returning the
error value otherwise.
@scolsen
Copy link
Copy Markdown
Contributor Author

scolsen commented Mar 24, 2022

I'm not sure about the /-> name for the result form. I wanted to use ||-> but the parser doesn't like it.

@scolsen scolsen requested a review from a team March 24, 2022 00:37
Copy link
Copy Markdown
Collaborator

@eriksvedang eriksvedang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

Copy link
Copy Markdown
Member

@hellerve hellerve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test or two for these? Also, any particular reasons you didn’t go for quasi-quoting here?

@scolsen
Copy link
Copy Markdown
Contributor Author

scolsen commented Mar 24, 2022

Can we add a test or two for these? Also, any particular reasons you didn’t go for quasi-quoting here?

No good reason, I just haven't rewritten my muscle memory yet 😅. Tests are a good idea, I'll add them + convert to quasi since it's more concise!

@TimDeve
Copy link
Copy Markdown
Contributor

TimDeve commented Mar 24, 2022

I originally thought these were for chaining fallible operations (each step would return Result/Maybe), wondering if that would be more powerful.

@scolsen
Copy link
Copy Markdown
Contributor Author

scolsen commented Mar 24, 2022

I originally thought these were for chaining fallible operations (each step would return Result/Maybe), wondering if that would be more powerful.

It would be, and I would personally love something equivalent to all of Kotlin's ? ?: stuff

I guess in theory these operators can be expressed in terms of that more general operator?

Comment thread core/ControlMacros.carp
("threads the value contained in a Result through the following" false)
"forms iff, the Result is a Success"
""
"Returns the erorr if the Result is Result.Error.")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo here: erorr.

@eriksvedang
Copy link
Copy Markdown
Collaborator

So if the composed functions produce a Nothing/Error in the middle of the chain, it is not aborted? (I didn't read the code very properly the first time, it seems)

@scolsen
Copy link
Copy Markdown
Contributor Author

scolsen commented Apr 4, 2022

So if the composed functions produce a Nothing/Error in the middle of the chain, it is not aborted? (I didn't read the code very properly the first time, it seems)

These macros just perform wrapping and unwrapping once and expect forms that operate on the interior value, so if you pass in a function that returns a result, your final value will have type Result a (Result b c). That is, it won't know about intermediate results -- its more like an if statement, if the initial value is ok, do all the things, otherwise dont

I think @TimDeve was asking for a monadic bind, where the forms/functions you pass in also return the Result type so that you can chain them. I do have this in my typeclass lib but it might be nice in core too

@eriksvedang
Copy link
Copy Markdown
Collaborator

OK; thanks for the clarification. I have a feeling people will expect these to be monadic.

@TimDeve
Copy link
Copy Markdown
Contributor

TimDeve commented Apr 4, 2022

The macro is kinda like doing apply + thread (without the lambda):

(Maybe.apply (Maybe.Just x)
             &(fn [y] (=> y
                          (+ 3)
                          (* 2))))

@eriksvedang
Copy link
Copy Markdown
Collaborator

I see. I think the threading macros should do the same thing on each "step".

@scolsen
Copy link
Copy Markdown
Contributor Author

scolsen commented Apr 4, 2022

yeah, let's just make it a monad I think. here's the way it's implemented in typeclass (where pure is Maybe.Just):

(doc >>=
    ("Expands into a form that sequences functions in a monadic context " false)
    "using `bind`."
    "```"
    "(>>= (Maybe.Just 1)"
    "     &(fn [x] (pure (+ 2 x)))"
    "     &(fn [x] (pure (+ 3 x))))"
    "=> (Just 6)"
    "```")
  (defmacro >>= [monad :rest functions]
    (Monad.bind-forms-internal 'bind (cdr functions)
      (list 'bind monad (car functions))))

We could basically make these macros the same, just dropping the requirement to pass a complete fn -- instead allowing the partial forms like -> takes.

The typeclasses lib also implements some haskell-esque do sugar:

(doc m-do
    ("\"do notation\" for monads--just like Haskell's do notation, this " false)
    "is sugar for binding the result of monadic actions to variables."
    ""
    "```"
    "(m-do [x (<- (Maybe.Just 1))"
    "       y (<- (Maybe.Just 1))]"
    "  (pure (+ x y)))"
    "=> (Maybe.Just 2)"
    "```")
  (defmacro m-do [bindings actions]
    (if (or (not (array? bindings))
            (not (= 0 (- (length bindings) (* 2 (/ (length bindings) 2))))))
        (macro-error
          (String.concat ["m-do bindings must be contained in an array, "
                          "which must contain an even number of "
                          "<variable> <binding> forms, "
                          "like `let` bindings: "
                          "[x (<- (Maybe.Just 2)) y (<- (Maybe.Just 1))]"]))
        (Monad.m-do-transform-bindings (reverse bindings) actions)))

@eriksvedang
Copy link
Copy Markdown
Collaborator

Seems like this got stuck in review limbo. Anyone has an opinion on whether to merge or close? (I'm leaning towards closing it).

@hellerve
Copy link
Copy Markdown
Member

I think the general idea is really cool! I think I’d try to update some of my libraries to use it so I can provide some actual feedback?

@eriksvedang
Copy link
Copy Markdown
Collaborator

Absolutely! Happy to leave it around 😃

@sqrew
Copy link
Copy Markdown
Contributor

sqrew commented May 5, 2026

these are at least partially covered in proposed pr 1545 with the suggested result-and-then/maybe-and-then
#1545

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants