Carp icon indicating copy to clipboard operation
Carp copied to clipboard

feat: add maybe and result thread macros

Open scolsen opened this issue 3 years ago • 10 comments

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

scolsen avatar Mar 24 '22 00:03 scolsen

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

scolsen avatar Mar 24 '22 00:03 scolsen

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!

scolsen avatar Mar 24 '22 14:03 scolsen

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

TimDeve avatar Mar 24 '22 16:03 TimDeve

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?

scolsen avatar Mar 24 '22 16:03 scolsen

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)

eriksvedang avatar Apr 04 '22 07:04 eriksvedang

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

scolsen avatar Apr 04 '22 14:04 scolsen

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

eriksvedang avatar Apr 04 '22 14:04 eriksvedang

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

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

TimDeve avatar Apr 04 '22 15:04 TimDeve

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

eriksvedang avatar Apr 04 '22 15:04 eriksvedang

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

scolsen avatar Apr 04 '22 15:04 scolsen