You can effectivelly use monads, even if you don’t know “categories”, “monoids”, “morphisms” or all the other funny words sprinkled around the Haskell documentation. This guide is intended for developers (thereafter “you”) who

This guide assumes you know programming, and specifically some functional programming. The examples in this guide are written in OCaml, a language with good support for (but no core dependency on) monads.

Bindings

One of the main abstractions of programming is binding:
naming a value.

(* binds the result of the multiplication to the variable name *)
let number_of_seconds_in_a_day = 60 * 60 * 24 ;;

Once a value has a name, the name can be used in place of the value.

let duration_in_seconds days = days * number_of_seconds_in_a_day ;;

It’s such a basic feature that we don’t often think about it. It’s such a basic feature but it’s also such a core feature of programming. Even more so in functional programming languages with immutable values.

Not-quite values

Occasionally, we have something similar to a value, but, like, not exactly, like, ish, like, there is some abstraction wrapped around the value, like, it’s boxed in some container, or like, it’s not available just immediately but only through some boilerplate-y layer of some kind. When you want to bind the honest salt-of-the-earth plain normal value underneath it all, you have to jump through a few hoops of syntax or safety or somesuch.

As a simple example, let’s start with optional values. You want a value, the programming language says it’s maybe a value but maybe nothing or none or null or whatever your programming language calls it. In this case you need to do a bit of legwork around the binding sites.

You end up writing code like the following.

let starting value =
   match Sys.getenv_opt "SEED" with
   | None -> None
   | Some seed_str ->
   match int_of_string_opt seed_str with
   | None -> None
   | Some seed -> Random.init seed; Some (Random.int 4096)
;;

You have to work around the option type’s None constructor, but what you really want is to get the value under the Some constructor. Also, you want to name the underlying value for later use.

Abstracting the bindings

When you are writing repeating binding-related boilerplate, you can likely use a monad instead. In the case of option above, you have the repeating general form

match <expression> with
| None -> None
| Some <variable name> -> <expression using the variable>

So you define

let bind x f =
    match x with
    | None -> None
    | Some x -> f x
;;

And now you can write

let starting value =
    bind (Sys.getenv_opt "SEED") (fun seed_str ->
    bind (int_of_string_opt seed_str) (fun seed ->
      Random.init seed; Some (Random.int 4096)))
;;

And that’s it. A monad. Pretty-much.

Syntax

Ok so you want your new abstraction to be usable? It needs a bit of syntax.

In OCaml, you can use binding operators to provide a nice experience.

let ( let* ) = bind ;;

And then you get

let starting value =
    let* seed_str = Sys.getenv_opt "SEED" in
    let* seed = int_of_string_opt seed_str in
    Random.init seed; Some (Random.int 4096)
;;

In Rust you get to use the let … ;? bindings as built-in for Result and Option (but you don’t get to define your own). In Haskell you get to use <- bindings in do blocks. In Racket you get to use <- bindings and monad-do blocks.

Some generalisation

Still no maths… but let’s take a slightly higher-level view for a bit.

Essentially, the monad has three parts:

First, the type. In the example above it’s 'a option. It indicates what kind of “not-quite-a value” you are handling, what kind of wrapper you have to reckon with.

Second, the bind function. It takes two parameters:

The bind function can be given some additional syntax niceties depending on the language.

Third, the return function. This wraps a normal value into a “not-quite-a value”. In the example above, we used the Some constructor directly. If the monad is on a concrete type you can use a constructor directly, otherwise you might have to go through a function instead.

As a general form you can write the following interface to a monad:

module type MONAD = sig
    type 'a t
    val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t
    val return : 'a -> 'a t
end ;;

And you can use it for the option type.

module OptionMonad
: MONAD with type 'a t = 'a option
= struct
    type 'a t = 'a option
    let ( let* ) x f = match x with None -> None | Some x -> f x
    let return x = Some x
end ;;

Depending on the monad, there might be a fourth part: run.

The run function “executes” the monad block. You don’t need it when the monad is based on a concrete type that you can inspect by hand. For example you don’t need a run function for the option monad because you can just check whether you get a None or a Some. But if the monad is based on an abstract type, you need a way to recover the underlying result.

What a monad “does”

There are monads for other not-quite-a-value abstractions. More importantaly, the monads do different things, they acheive different goals.

The option monad above is a straightforward control-flow monad which interrupts a computation if a value is missing.

let* seed_str = Sys.getenv_opt "SEED" in
(* only if the environment variable is set
   does the computation reach this point *)
let* seed = int_of_string_opt seed_str in
(* only if the conversion to int succeeds
   does the computation reach this point *)
Random.init seed; Some (Random.int 4096)

Result
The result monad is similar to the option monad in that it does the same thing. The only difference is that failures carry information.

let ( let* ) x f = match x with
  | Error e -> Error e (* e is error information *)
  | Ok v -> f v (* carry on if x is successful *)

List
If you want to apply your code to many possible values, you end up with some repeating patterns where you successively bind different values to the same variable. You can abstract this with a monad.

let ( let* ) x f =
  List.map f x (* apply f to each value in the list x *)
  |> List.concat (* combine all the results *)
;;

Or more succintly

let ( let* ) x f = List.concat_map f x ;;

You can then write code which applies to all the listed input values:

let treasure_map =
   let* colour = [ "red"; "green"; "blue" ] in
   let* item = [ "key"; "lock" ] in
   let random_coordinates =
      (* generate fresh coordinates for each different
         combination of colour and item *)
      gen_coordinates () in
   [ (random_coordinates, colour, item) ]
;;

This monad doesn’t really do control-flow. It’s more of a data-flow abstraction where multiple values are passed to the same function and the results are collected together.

The distinction between control-flow and data-flow monad is not necessarily relevant. Monads can be somewhere in between. For example,

Backtracking
If you write a simple exploration algorithm using backtracking, you end up with some repeating patterns of code where you bind candidates to variable names. (This example uses the Seq module to represent multiple possibilities.)

let ( let* ) x f = Seq.concat_map f x ;;

which you can then use to find a solution by exhaustively checking each candidate. For example, finding pairs of numbers which have the same sum and product:

let* a = Seq.ints 0 in
let* b = Seq.ints 0 in
if a + b = a * b then Seq.return (a, b) else Seq.empty

For this simple backtracking monad you can define a run function:

let run b = match b () with
  | Seq.Nil -> None
  | Seq.Cons (v, _) -> Some v

The list monad and the backtracking monad are very similar. In fact, the main difference is laziness in the evaluation: the list monad traverses all the input, the backtracking monad explores enough of the input to find a solution. You could even define the list monad as the backtracking monad with a different run function.

Ultimately it’s not very important. You can define your own dedicated monad even if there’s a common monad available in your programming language or its ecosystem.

Keep an eye for repeating binding-related boilerplate in your (your!) code.
Try to understand what this boilerplate does.
See if a monad is a good fit for abstracting it away.
Hopefully your code will be more readable for it.


Adendum: what’s with Haskell’s lazyness?

There’s a snarky remark that OCamlers point at Haskellers:

OCaml has monads but you don’t need them to tie up your shoes.

This remark gets to the fact that you can’t do (do?) anything in Haskell without using a monad. The fundamental reason is that Haskell is a lazy and pure language and you eventually need to break away from these features/restrictions. The way in which you break away is baked into the language as monads.

The one that I’ve seen puzzle people more is the lazyness and the IO monad. Well lazyness is an abstraction which wraps the real salt-of-the-earth values into a box. The box is a memoised computation: you get to the real value by running the computation or reading off the memoised result of the run.

Like you don’t have an integer, you have the formula to compute it. Or like maybe you already have an integer if it’s already been computed, you don’t know.

Like you haven’t written bytes to a file, you have the sequence of instructions to compute it. (Or maybe you’ve already written the bytes if the instructions have already been executed.)

The IO monad is a way to organise the lazy computations and instructions. Values wrapped in this way are eventually forced or ignored.

You can have the same monad in OCaml. It’s not particularly useful unless you write a lot of (explicitly) lazy code.

module LazyMonad
: MONAD with type 'a t = 'a Lazy.t
= struct
    type 'a t = 'a Lazy.t
    let ( let* ) x f = f (Lazy.force x)
    let return x = lazy x
end ;;