Earlier this year, we created a ppx_let, a PPX rewriter that introduces a syntax for working with monadic and applicative libraries like Command, Async, Result and Incremental. We’ve now amassed about six months of experience with it, and we’ve now seen enough to recommend it to a wider audience.

For those of you who haven’t seen it, let syntax lets you write this:

```
let%bind <var> = <expr1> in <expr2>
```

instead of this:

```
<expr1> >>= fun <var> -> <expr2>
```

with analogous support for the map operator, via `let%map`

. The choice of monad
is made by opening the appropriate `Let_syntax`

module, *e.g.*, you might open
`Deferred.Result.Let_syntax`

, or `Incr.Let_syntax`

. Note that `Async.Std`

now
opens `Deferred.Let_syntax`

by default.

There’s also support for match statements, e.g.:

```
match%bind <expr0> with
| <pattern1> -> <expr1>
| <pattern2> -> <expr2>
```

is equivalent to:

```
<expr0> >>= function
| <pattern1> -> <expr1>
| <pattern2> -> <expr2>
```

There’s also support for parallel binds and maps, using the `and`

syntax. So
this:

```
let%map <var1> = <expr1> and <var2> = <expr2> in <expr3>
```

is (roughly) equivalent to this:

```
map3 <expr1> <expr2> ~f:(fun <var1> <var2> -> <expr3>)
```

This pattern generalizes to arbitrarily wide maps. It’s implemented using `map`

and the `both`

operator, which sacrifices some performance in exchange for
generality, vs the explicit `mapN`

operators.

# Advantages

My experience with the new syntax has been quite positive. Here’s my summary of the wins.

## Parallel binds

For libraries like `Command.Param`

and `Incremental`

, where multi-way map
functions (like `map2` and `map3`) are important, it’s been a
pretty big win in terms of the comprehensibility of the resulting code. This
tends to be the case for applicatives like `Command.Param`

, which are just
monads without bind. The big advantage is that by writing:

```
let%map x1 = some_very long expression
and x2 = some_other long expression
and x3 = yet another_thing
in
x1 + x2 / x3
```

we get to put the variable names directly next to the expressions they’re being bound. Using an explicit mapN operator, the result is more awkward:

```
map3
(some_very long expression)
(some_other long expression)
(yet another_thing)
~f:(fun x1 x2 x3 -> x1 + x2 / x3)
```

This is error prone, since it’s easy to mix up the variables and the
expressions. To avoid the corresponding issue in the original Command library,
we used some fancy combinators and the dreaded `step`

operator, leading to some
hard to understand code. The let-syntax equivalents are materially easier to
use.

## Variables first

Using a let-like syntax lets you put the variable before the definition, which follows the pattern of ordinary OCaml code, and makes it a bit easier to read. This cleans up some otherwise awkward patterns that are pretty common in our code. In particular, instead of this:

```
begin
<expr1>;
let <var> = <expr2> in
<expr3>
end
>>= fun meaningful_variable_name ->
<expr4>
```

You can write this:

```
let%bind meaningful_variable_name =
<expr1>;
let <var> = <expr2> in
<expr3>
in
<expr4>
```

which flows a bit more naturally, in part because the meaningful variable name
comes first, and in part because the extra `begin`

and `end`

are dropped.

## Connecting bind to let

Let binds are a lot like monadic binds, even before you add in any special
syntax. *i.e.*, this

```
<expr1> >>= fun x -> expr2
```

is a lot like this.

```
let x = <expr1> in <expr2>
```

This is why monads are sometimes described as “programmable let-binds” (or, relatedly, “programmable semicolons”, which are just let-binds with a unit argument.)

I’ve found this to be a useful analogy in understanding monads, and the analogy is made clearer with let syntax. We have some preliminary reports of this making monadic code more approachable for beginners, which lines up with my intuition.

The similarity between ordinary lets and monadic lets also makes diffs easier to
read. *e.g.*, in Async, if some function goes from being synchronous to
deferred, the change at the call point would now be from this

```
let x = some_synchronous_thing () in
more things
```

to this.

```
some_asynchronous_thing ()
>>= fun () ->
more things
```

With let-syntax, we would instead change it to this.

```
let%bind x = some_asynchronous_thing () in
more things
```

*i.e.*, the only thing that would change would be the addition of `%bind`

. The
resulting diff is more targeted, making the substance of the change a bit easier
to see, making refactoring that adds or remove blocking easier to do and
understand.

# Disadvantages

It’s not all wine and roses. There are some downsides to let-syntax:

## It’s new and different

Enough said.

## It’s kinda ugly

This is a matter of taste, but I’ve heard some distaste for the percent sign itself. That’s something forced on us by PPX, but I don’t exactly disagree.

Also, the `%bind`

and `%map`

are a little wordy. There’s been some talk of
adding the ability to define alternate let syntaxes in OCaml proper, which would
allow you to write something like this.

```
let* x = some_asynchronous_thing () in
let* y = some_other_thing () in
let+ z = a_third thing in
x + y + z
```

Here, `let*`

would be equivalent to `let%bind`

, and `let+`

is equivalent to
`let%map`

. Again, it’s not clear to me that this would all in be a win.

I personally find the new syntax all in less ugly than using infix operators everywhere, but again, tastes vary.

## Unit binds aren’t great

In particular, because we have no “monadic semicolon”; in the syntax, you have to go from this:

```
<expr1>
>>= fun () ->
<expr2>
```

to

```
let%bind () = <expr1> in
<expr2>
```

which is not ideal, since it’s not parallel to the normal semicolon syntax for this outside of the monad. We’ve looked at making it possible to do something like:

```
<expr1> ;%bind
<expr2>
```

which would be more parallel with ordinary OCaml syntax, but that’s not yet possible, and it’s not clear it’s a net win.

## It changes how you think about interleaving in Async

In Async, when you write:

```
load_something ()
>>= fun x ->
process_thing x
```

you can think of the point where interleaving can happen as the place where the bind operator is found. With let-syntax:

```
let%bind x = load_something () in
process_thing x
```

the location is different, and somewhat less obvious. My experience has been that this was easy to adjust to and hasn’t tripped me up in practice, but it’s a concern.

# Idioms

A few thoughts on how to use let syntax effectively.

## Let syntax for variables, infix for point-free

One might wonder whether there’s any use for the infix operators once you are
using `Let_syntax`

. I believe the answer is yes. In particular, the style we’ve
adopted is to use let syntax when binding a variable.

```
let%bind x = some_expression in
```

and infix operators when going point-free, *i.e.*, when not binding variables.

```
let v = some_function x >>| ok_exn in
```

One special case of this is binding unit, where some people prefer to use the following pattern, since we don’t have a nice monadic semi-colon yet.

```
let%bind x = some_operation in
some_other_operation >>= fun () ->
let%bind y = yet_another_thing () in
a_final_thing y
```

rather than:

```
let%bind x = some_operation in
let%bind () = some_other_operation in
let%bind y = yet_another_thing () in
a_final_thing y
```

## Mixing monads

One change we made recently was to add the return function and the monadic infix
operators to the `Let_syntax`

module that one opens to choose a monad. This has
the useful property of causing one to basically switch cleanly from one monad to
another when you open the `Let_syntax`

module. Mixing multiple monads in the
same scope is hard to think about.

### Command.Param and Deferred

A few interesting cases that come up are mixing the `Command.Param`

syntax with
the `Deferred`

syntax. This one is pretty easy to solve, because you don’t
typically need to mix them together, really. It’s just that in the body of the
command, you often want `Deferred`

, but in the definition of the command line
parser, you want to use `Command.Param`

. This can be handled by doing a local
open of `Command.Param.Let_syntax`

or `Deferred.Let_syntax`

as necessary.

### Deferred and Deferred.Result

A more complicated case is choosing between the `Deferred`

and `Deferred.Result`

monads. In Async, there are infix operators that let you use both sets of bind
and map operators (basically, with question-marks at the end of the ordinary
infix operators for the `Deferred.Result`

operators.)

Mixing these operators together in a single scope can be a little awkward, often
leaving people to add and remove question-marks until things compile. With let
syntax, you really have to pick a single monad, which is easier to read, but
then requires some changes in behavior. In particular, you often need to move
things from one monad to another. For example, if you’re in the `Deferred`

monad
and get a result of type `Deferred.Or_error.t`

, you might want to do something
like this:

```
let open Deferred.Let_syntax in
let%bind v = some_operation x y >>| ok_exn in
```

Here, mapping over `ok_exn`

will take the error and raise it, if necessary.
Similarly, if you’re using an operation that’s in the ordinary `Deferred`

monad
but you’re operating in the `Deferred.Result`

monad, you might want to lift that
operation up, *i.e.*:

```
let open Deferred.Result.Let_syntax in
let%bind v = some_other_operation x y |> Deferred.map ~f:(fun x -> Ok x) in
```

This is something of a mouthful, so we just added the `Deferred.ok`

function, so
on our latest release you can write:

```
let open Deferred.Result.Let_syntax in
let%bind v = some_other_operation x y |> Deferred.ok in
```

This idiom is useful is useful whether or not you’re using let syntax.