The next major release of the OCaml compiler, version 4.08, will be equipped with a new syntax extension for monadic and applicative composition. Practically it means that it will be a bit more convenient to work with APIs structured around these patterns. The design draws inspiration from ppx_let but offers lighter syntax, and removes the need of running the code through a ppx preprocessor.
Compared to similar extensions for languages like Haskell, F# and Scala, it’s interesting to note that the OCaml version is not only targeting monads but also supports a version for applicative functors.
This post contains some concrete examples of what the new syntax looks like and how to enable it.
As of writing, version 4.08 has not been officially released so in order to follow along you need to update your opam and switch to the beta release:
opam switch ocaml-variants.4.08.0+beta1
Alternatively, you can also use the latest version of dune
for building, which
has a backport of the syntax extension.
An example - working with options
The examples below are about composing functions returning optional results using the monad and applicative functor combinators for options.
To have something concrete to work with, assume the following API:
val safe_head : 'a seq -> 'a option
val safe_tail : 'a seq -> 'a seq option
val safe_div : float -> float -> float option
Monad syntax for options
The monadic composition combinators for the option
type may be defined
as a function bind
with the signature:
val bind : 'a option -> ('a -> 'b option) -> 'b option
Implemented as:
let bind o f =
match o with
| Some x -> f x
| None -> None
The convention is to also provide an infix version, as in:
let ( >>= ) o f = bind o f
It’s effectively used for composing sequences of functions yielding optional results.
As a silly example, consider writing a function that given a float seq
,
returns the value of the first two elements divided, if the sequence contains
at least two elements, and the division is successful. Using the API from
above and the monadic bind operator, it may be defined as:
let div_first_two xs =
safe_head xs >>= fun x ->
safe_tail xs >>= fun ys ->
safe_head ys >>= fun y ->
safe_div x y
We can write this differently using the syntax extension for monads, all that
is needed is another alias to bind
:
let (let*) x f = bind x f
That’s it, the same program can now be expressed as:
let div_first_two xs =
let* x = safe_head xs in
let* ys = safe_tail xs in
let* y = safe_head ys in
safe_div x y
In general, the expression:
let* x = e1 in e2 x
desugars to the equivalent of:
e1 >>= fun x -> e2 x
Applicative syntax for options
Monads are great for composing dependent computations but not all
compositions are dependent. For the cases where monads are either an
overkill or just not feasible, applicative functors provide an alternative. You
can find some more information about this pattern in the context of OCaml,
here;
They are often described in terms of two functions pure
and
apply
, with the signatures:
val pure : 'a -> 'a t
val apply : ('a -> 'b) t -> 'a t -> 'b t
The infix version of apply
is usually called (<*>)
, i.e.:
let ( <*> ) fa xa = apply fa xa
How exactly do they supplement monads? By looking at the signature of
apply
, it is clear that in the expression, f <*> x
, both f
and x
are
applicative values that exist before the evaluation of apply
is performed.
In contrast, in the monadic composition expression, x >>= f
, the monad
value produced by f
is not known until it’s actually applied a value extracted
from x
.
We, therefore, say that applicatives provide static composition whereas monads also support dynamic composition. In short, this makes monads more powerful but less optimization friendly.
Below are the traditional applicative combinators defined for the option
type:
let pure x = Some x
let apply fo xo
match fo, xo with
| Some f, Some x -> Some (f x)
| _ -> None
let (<*>) fo xo = apply fo xo
And, here’s an example of how they’re used in defining a function that given
three float seq
values, adds their heads together, in case they’re all non-empty:
let add_heads xs yz zs =
pure (fun x y z -> x +. y +. z)
<*> safe_head xs
<*> safe_head ys
<*> safe_head zs
This function could of course also be written in a monadic style but what’s
nice about the applicative version is that it’s apparent from the definition that
there are no dependencies between the three calls to safe_head
.
If we were operating in some other applicative context, say Async.t
, we could in
fact run the extractions in parallel.
As for the OCaml syntax extension for applicatives, it’s actually not based on
apply
and pure
, but a pair of alternative combinators, often called map
and product
:
val map : ('a -> 'b) -> 'a t -> 'b t
val product : 'a t -> 'b t -> 'a * 'b t
To show that apply
and product
are equivalent, here’s how you define
the infix version of apply
in terms of map
and product
:
let ( <*> ) fa xa = map (fun (f, x) -> f x) @@ product fa xa
Going the other way around, one can also express product
using pure
and apply
:
let product xa ya = pure (fun x y -> (x, y)) <*> xa <*> ya
The implementations of map
and product
for the option
type are straight forward:
let map f = function
| None -> None
| Some x -> Some (f x)
let product o1 o2 =
match o1, o2 with
| Some x, Some y -> Some (x,y)
| _ -> None
Now, to enable the special syntax we just need to define (let+)
as map
with arguments
in reversed order and (and+)
as an alias for product
:
let (let+) x f = map f x
let (and+) o1 o2 = product o1 o2
Finally, using the syntax extension, we can rewrite the example above, like this:
let add_heads xs yz zs =
let+ x = safe_head xs
and+ y = safe_head ys
and+ z = safe_head zs in
x +. y +. z
Again, the syntax stresses how the three safe_head
computations are
independent. In fact, the compiler prevents us from introducing any
dependencies (just like with regular) let .. and
syntax).
In general, an expression, let+ x = e1 and+ y = e2 in e3 x y
, gets desugared to:
map (fun (x,y) -> e3 x y) (product e1 e2)
Or the equivalent expression, written in terms of apply
and pure
:
e3 <$> e1 <*> e2
Here, (<$>)
is the infix version of map
.
Wrapping up
To conclude the examples for option
s, here’s a version where the operations
are wrapped in a module Option
with the syntax extensions contained in a
sub module. This allows users of the library to opt in for the new
syntax when needed (by opening Option.Syntax
):
module Option = struct
let map f = function
| None -> None
| Some x -> Some (f x)
let bind o f =
match o with
| Some x -> f x
| None -> None
let product o1 o2 =
match o1, o2 with
| Some x, Some y -> Some (x,y)
| _ -> None
module Syntax = struct
let (let+) x f = map f x
let (and+) o1 o2 = product o1 o2
let (let*) x f = bind x f
end
end
So, whenever you wish to extend your data type, t
, with
special syntax, implement a module like Syntax
, with the following
signature:
module Syntax : sig
(* Monad *)
val ( let* ) : 'a t -> ('a -> 'b t ) -> 'b t
(* Applicatives *)
val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
val ( and+ ) : 'a t -> 'b t-> ('a * 'b) t
end
If you’re only able to support the applicative subset, simply skip ( let* )
.
Final notes
As with any language extension, there is a tradeoff between the utility introduced and the additional complexity or cognitive load required in order to benefit from it. In this case I think it’s worth the price. In particular, it’s likely going to nudge developers toward the time-tested patterns of monads and applicative functors. Having more libraries with common looking APIs actually works in the opposite direction, reducing the amount of cognitive load required.