The option monad and let*
Module 8 is about two ideas that recur all over real OCaml code. The first, which occupies this lecture and the next two, is the monad: a small design pattern for sequencing computations of a particular shape without writing the plumbing by hand. The second, which the module turns to after the monads, is GADTs, a type-system feature for giving variants more precise types. The two come together in the closing tutorial.
The word monad sounds scarier than it is. The mathematical
machinery lives in category
theory, which is a
beautiful subject but not what we are doing here. For programming,
a monad is just a type plus two operations (return and bind)
that let you chain computations of one shape cleanly. This lecture
builds that pattern from a concrete pain point and lands on OCaml's
let* syntax for it.
The pattern has an unusually clean paper trail. Eugenio Moggi, a
semanticist, showed in 1989 that a single categorical structure
could describe computational effects (exceptions, state, I/O) in
a uniform way. Philip Wadler read the mathematics and saw a
programming technique: his 1992 paper
The essence of functional programming
showed working interpreters where adding error handling or state
meant changing a few lines around an unchanged core, and monads
jumped from category theory into Haskell, then into every
functional language. What you will build in this lecture is
Wadler's move, replayed on OCaml's option.
A motivating problem
We ended the Module 5 tutorial with a
little expression evaluator built out of nested matches on
option: every
sub-result was unwrapped with a Some _/None arm, and None
propagated by hand. That boilerplate is the problem this lecture
solves.
Here is the pattern in miniature. Suppose we parse a number from a
string, double it, check it is in range, and print it. Each step
might fail, so each returns an
'a option:
Some x on success, None on failure.
Each None here marks a real failure mode. parse_int fails when
the string is not a number. double can overflow: native OCaml's
int is 63-bit, but the same code compiled to JavaScript with
js_of_ocaml uses 32-bit ints
(this is exactly how the runnable cells on this page execute), so
doubling a large value silently wraps to a wrong answer. We refuse
it and return None rather than lie. small fails when the result
is out of range. Only print_num always succeeds, but it still
returns Some () so that every step shares the same 'a -> 'b option shape: that uniformity is exactly what lets us chain them.
We want to wire them together: parse, then double, then check, then
print. At each step, if the previous step said None, the whole
pipeline gives up; if it said Some x, we feed x to the next
step. The naive translation nests a match per step:
Every step has exactly the same shape: "if the previous step is
None, return None; otherwise unwrap and continue." It is
written once, then twice, then three times, growing linearly with
the number of steps. That repetition is the signal to pull out an
abstraction.
Capturing the pattern: bind
Lift the repetition into a function. It takes an option and a
continuation saying "what to do if we have a value", and returns
the next option. With it the four nested matches flatten to a
vertical sequence, one bind per step:
OCaml infers bind's type as 'a option -> ('a -> 'b option) -> 'b option: given the previous step's result and a continuation that
produces the next option, it returns the combined,
possibly-short-circuited result. The two type variables are
independent because the value type changes from step to step
(string to int, int to int, and so on).
bind collapses the three None -> None arms into one definition,
but it is still noisy: each line opens a parenthesis that piles up
at the end, and bind ... (fun x -> ...) is heavier than let x = ... in .... OCaml has one more piece of sugar that closes the gap.
Monad definition
A monad is a type plus two operations. Concretely for option:
return (sometimes called pure) is the trivial way to put a
plain value into the option world. It earns a name because it is
part of the monad interface: anything claiming to be a monad
provides return and bind, and the rest of the code can pretend
not to know which monad it is using.
The line let ( let* ) = bind is where the magic happens. The
identifier ( let* ) is a let-operator (an OCaml feature since
4.08). Any identifier let X whose X starts with a punctuation
character can be bound to a function; once it is in scope, the
compiler treats let* x = e in rest as sugar for ( let* ) e (fun x -> rest), which is bind e (fun x -> rest). That single
rewrite rule is the whole feature.
Using let*
The pyramid, one more time, now with let*:
The compiled code is identical to the nested-match and
explicit-bind forms: let* is purely syntactic sugar that
elaborates back to bind. A reader learns to read let* as "this
might fail, and if it does, give up"; each let* line names the
successful result, and the short-circuit on failure is implicit.
The whole function reads top to bottom with no nested matches.
let* is not magic
We just used let*; here is why it is not magic. Ordinary let
works the same way. Name reverse application flip (so flip a f = f a): then let x = e in rest is flip e (fun x -> rest), and
let* has the identical shape, swapping flip for bind:
So reading let* is no harder than reading let: bind a name,
then continue. The extra behaviour is invisible at the call site
and lives entirely in the monad's bind.
Option.bind and Option.map
The standard library ships these, so you do not write them yourself in real code:
In monad-speak, map is the functor operation and bind the
stronger monad operation; map can be defined as bind opt (fun x -> Some (f x)). There is also a sibling let-operator, ( let+ ),
for map-only chains where no new failure is introduced; you will
meet it in real code, but let* alone covers everything we need
here.
In practice, codebases that lean on monads define a small module
per monad with bind, ( let* ), and helpers, then let open M in at the top of a function so the rest reads as plain let*. The
compiler does not guess which monad you mean: you choose it by what
is in scope, and the type of let* is always right there in front
of you.
A realistic example: reaching into optional data
A user may have no address, and an address may have no zip.
To get a user's zip we must descend through both options. This is
the pattern other languages bake into syntax as optional chaining
(a?.b?.c in Swift, Kotlin, or C#; &. in Ruby); in OCaml it is
just let* over option:
This is where let* genuinely beats a single match: because each
step depends on the previous one's value, you cannot lift the
checks into one flat match. It is the safe-navigation idiom that
option plus let* gives you for free, the antidote to the null
dereference Module 4 warned about.
When not to use a monad
A monad is overkill for a single optional step. The plain match
is shorter and just as clear:
There is nothing magic about three; it is a rule of thumb. If you
find yourself indenting past column 50 to handle a third level of
None, switch to let*. (A lawful monad's return and bind
also satisfy three equational laws; we make those precise in the
next lecture.)
Where else does this come up?
The pattern is worth learning because the same 'a t + return +
bind shape recurs everywhere, with t different each time:
Build the habit of asking "is this shape a monad?" whenever you write a computation that may not produce a plain value, and you will spot the pattern far more often than you would expect.
A quick check
What is the type of the helper bind we defined?
'a option -> 'a option -> 'a option'a option -> ('a -> 'b option) -> 'b option'a -> ('a -> 'b option) -> 'b option'a option -> ('a -> 'b) -> 'b option
Why: bind takes an option (the previous step's result), a
function that turns the unwrapped value into the next option, and
returns that next option. The two type variables 'a and 'b are
independent because the value type can change from step to step
(parse a string to an int, then double the int, etc.).
You have let* x = e1 in let* y = e2 in let* z = e3 in Some (x, y, z), where e1 is Some 1, e2 is None, and e3 is some
expression. What does the whole thing evaluate to, and how many
times is e3 evaluated?
Some (1, ?, ?), evaluated once.None, evaluated zero times.Some (1, _, _), evaluated zero times.- An exception is raised.
Why: the option monad short-circuits on the first None. Once
e2 is None, the surrounding let* returns None without
evaluating its continuation, so e3 never runs. Failure is
detected as soon as it happens and downstream code is skipped.
Show reference solution
A code quiz to consolidate:
Using the given bind and safe_div, write div_chain : int -> int -> int -> int option that computes a / b / c, returning
None if either division is by zero.
Show reference solution
Reference solution:
let div_chain a b c =
bind (safe_div a b) (fun ab ->
safe_div ab c)
Two divisions, one bind, then a tail call. The chain
short-circuits on the first divide-by-zero. Sugared with let*
this reads let* ab = safe_div a b in safe_div ab c.
What is next
The next lecture states the three
laws that make a monad well-behaved, then shows the same let*
notation driving two very different shapes: the list monad (many
values) and the result monad (a value or an informative error).
Reading
- Cornell CS3110, Monads: https://cs3110.github.io/textbook/chapters/ds/monads.html
Sources
This lecture's prose, worked examples, and quizzes are original to
this course. Materials referenced during preparation are listed in
the Reading section above; Cornell CS3110 and Real World OCaml
are CC BY-NC-ND-licensed and have not been derivatively reused.
See LICENSES.md
at the repository root for the full source posture.