Is there an easy way to understand (not memorize) that Result.bind is non-curried?

Hey all.

Recently, I was confused by 2 different examples from the documentation:

(from here)

let email = "ocaml.mycamel@ocaml.org"
;;

email
|> String.split_on_char '@'
|> Fun.flip List.nth 0
|> Option.map (fun str -> String.sub str 0 5)
|> Option.get
;;

and (from here)

let email = get_user ()
           |> Result.bind get_email
           |> Result.bind extract_domain
           |> Result.bind validate_domain
           ;;

In the first case, email is passed as a 1st arg to the function, that is return from the partial application String.split_on_char '@'.
In the second case, email is passed as a 1st arg to Result.bind directly, get_email function is passed to the 2nd arg of it. So, the Result.bind get_email expression is not treated as a partial application.

Nothing in the API documentation or the Result.bind signature can tell that Result.bind is non-curried and that allows it to be used for chaining in the way shown above.

My question is: is it possible to deduce the nature of the function like Result.bind (curried or not) in such a case?

Your second example is introduced by the following paragraph:

Unfortunately, calling Result.bind can be a bit awkward. We can’t pipe our value through a series of binds like we can do with calls to map. For example, this isn’t valid:

2 Likes

The issue with Result.bind isn’t related to currying, it’s about the order of arguments. The pipe operator x |> f reduces to function application f x, so get_user () |> Result.bind get_email is the same as Result.bind get_email (get_user ()). Unfortunately, Result.bind takes the result value first and the function second, which we can see in the type signature val bind : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result, so the piped code doesn’t work. The rest of the example is about playing a trick with let operators to make using bind a little nicer syntactically.

Result.bind is in fact a curried function. An uncurried version would take a tuple of arguments, and have a signature like this: val bind_uncurried : (('a, 'e) result * ('a -> ('b, 'e) result)) -> ('b, 'e) result.

4 Likes

Aha, I missed that! So, it was an invalid illustrative example. Thanks for pointing me to that!

1 Like

To be honest this is not a very convincing argument for the benefits of let-operators since it would be sufficient to define a flipped version of bind to use piping with results, or alternatively define an infix version of bind as it is in fact standard to do:

get_user ()
>>= get_email
>>= extract_domain
>>= validate_domain

The actual point of let-operators is that you can intersperse the result bindings with regular bindings and with control flow constructs that depend on the bound elements, without having to pre-define auxiliary functions or ending up with a hard-to-read pile of anonymous functions.

5 Likes

We can also write

let b = Fun.flip Result.bind

get_user ()
|> b get_email
|> b extract_domain
|> b validate_domain

But sure, the infix >>= operator is more natural.

Imho, the actual point of let-operators is flipping back the right-to-left callback style into the more natural-looking left-to-right assignment style. With callbacks code looks backwards. With letops it looks like natural, direct assignments again.

Yeah, I also thought of a flipped Result.bind

In some languages the flipped version is called flat_map, join_map or concat_map (just mentioning in case it helps connect some ideas for people). I think it would be helpful to have the flipped operators defined in the Stdlib for types like Option and Result, but I know OCaml tends towards minimalism for things like this. :sweat_smile:

1 Like

I try to add it into the Stdlib but it had some acceptable oppositions: Add flat_map in stdlib by xvw · Pull Request #12774 · ocaml/ocaml · GitHub

Imho, in the Stdlib it would be useful to have the most general-possible, let-operator, let ( let@ ) = ( @@ ), and have a convention of making the function argument the last parameter. I explain the reasoning here: letops/letops.mli at 6954adb65f1156597405149d5d0116599b1d2049 · yawaramin/letops · GitHub

3 Likes

I know that this isn’t how the operators and style evolved, but I’ve always understood the bind operator in terms of Danvy’s three-step explanation of CPS via Felleisen’s A-translation.

(1) start with a direct-style program

... F (M N) ....

(2) A-translate it

let f = F in
let x = M in
let y = N in
let z = x y in
...

(3) then flip into CPS by “squinting”

F (\f .
M (\x .
N (\y .
(x y) (\z .
...... ) ) ) )

And with CPS.bind : (('m -> 'a) -> 'a) -> ('m -> 'a) -> 'a you get

CPS.bind F (\f .
CPS.bind M (\x .
CPS.bind N (\y .
CPS.bind (x y) (\z .
...... ) ) ) )

And the generalization to other monads is to just change from CPS.bind to Result.bind (or whatever).

So the reason I always remember for why XX.bind has the type it does, is that it makes the “squinting” operation in step #3 easiest to do manually (before the advent of syntax support via letops in the parser).

ETA: and finally, in standard semantics the continuation always comes last. And that function argument to bind is really a continuation.

4 Likes