Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add maybe and result thread macros #1403

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

scolsen
Copy link
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
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
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
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
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
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
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?

("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.")

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
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
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
Collaborator

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

@TimDeve
Copy link
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
Collaborator

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

@scolsen
Copy link
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)))

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.

5 participants