blogs
Monads in Programming

Monads in Programming

· updated

As Douglas Crockford once mentioned in his talk, once you understand the monads, you lose the ability to explain them. The word itself tends to obscure what is, at its core, a remarkably practical idea about composing computations.

Where the word came from

Monads came out of algebraic topology, not programming. In the late 1950s, Roger Godement described a construction he called the standard construction1: a functor TT together with two natural transformations that let you wrap things up and flatten double wrappings. Topologists used it to build resolutions for computing cohomology. It was a tool, not yet a subject.

Through the 1960s the structure got several names. Peter Huber and others called it a triple, because it consists of three things: a functor and two transformations. In 1965 two papers appeared almost back to back, one by Heinrich Kleisli2 and one by Samuel Eilenberg and John Moore3, and they answered the same question in two different ways: does every monad come from a pair of adjoint functors? (Yes, and we’ll see both answers below.)

The word monad itself is usually credited to Jean Bénabou, and the story that gets told is that it came up around the category theory meetings of the mid-60s, where people were tired of “triple” and “standard construction” and wanted something better. It’s a blend of monoid and triad, and there’s a nod to Leibniz’s Monadology in it too. Saunders Mac Lane picked the name for Categories for the Working Mathematician4 in 1971, and that pretty much settled it, though you’ll still find “triple” in older papers.

Then nothing much happened in programming for about twenty years. In 1989 Eugenio Moggi5 pointed out that many “impure” features of programs (exceptions, state, nondeterminism, input/output) could be described uniformly as monads, and that this gave a clean semantics for them. Philip Wadler6 took Moggi’s idea and showed functional programmers how to write code with it. By 1996, Haskell 1.3 used monads for I/O, and the idea escaped into the wider world.

What is a monad, informally

A lot of the time, functions don’t just return a value. They return a value plus something: a value that might be missing, a value with a log attached, a value that depends on some state, a list of possible values. Call that “something” a context, and write TAT A for “a value of type AA in context TT”.

A monad is what you need to chain functions like this together without the context getting in your way. That needs exactly two things:

  1. A way to put a plain value into the context, without doing anything interesting. Call it η\eta (in code: return or pure).
  2. A way to take a context-wrapped value and feed it into a function that produces another context-wrapped value. That’s bind, and we’ll spend a whole section on it.

Before going further, let’s look at two concrete examples, the abstract version will make much more sense once you’ve seen it in practice.

The Maybe monad

The simplest context is “this might not be there”. Mathematically, given a type AA, we add one extra point to it, call it nothing:

Maybe A=A+1\text{Maybe}\,A = A + 1

Here ++ is disjoint union and 11 is a one-element set. A value of Maybe A\text{Maybe}\,A is either just(a)\text{just}(a) for some a∈Aa \in A, or nothing\text{nothing}.

Putting a value in the context is obvious: η(a)=just(a)\eta(a) = \text{just}(a).

Chaining is where it earns its keep. Suppose I have

f:A→Maybe B,g:B→Maybe Cf : A \to \text{Maybe}\,B, \qquad g : B \to \text{Maybe}\,C

We can’t just compose them as g∘fg \circ f, because ff hands back a Maybe B\text{Maybe}\,B and gg wants a plain BB. What we want is: if ff gave us just(b)\text{just}(b), run gg on bb; if ff gave us nothing\text{nothing}, stop and return nothing\text{nothing}. That rule is the entire Maybe monad. It’s the “early return on null” pattern you’ve written a thousand times, written once.

The State monad, and a detour through currying

The second example needs a small detour, because it relies on currying, and currying turns out to matter a lot later.

Currying transforms a multi-argument function into a sequence of single-argument function applications, where each intermediate application returns a function awaiting the remaining arguments.
Currying transforms a multi-argument function into a sequence of single-argument function applications, where each intermediate application returns a function awaiting the remaining arguments.

Currying is the observation that a function of two arguments is the same thing as a function that takes the first argument and returns a function of the second:

Hom(A×S, C)  ≅  Hom(A, CS)\text{Hom}(A \times S,\ C) \;\cong\; \text{Hom}(A,\ C^S)

Here CSC^S means “functions from SS to CC”. Going from left to right is called currying, going back is uncurrying. In category theory language, this says the functor −×S- \times S is left adjoint to the functor (−)S(-)^S. Keep that word, adjoint, in the back of your mind; it’s coming back.

Now, a stateful computation is one that reads some state s∈Ss \in S, produces a result, and produces a new state. As a pure function:

StateS A=S→(A×S)=(A×S)S\text{State}_S\,A = S \to (A \times S) = (A \times S)^S

Look at the shape of that: it’s (−×S)(- \times S) followed by (−)S(-)^S. It is literally the two halves of the currying adjunction glued together. That is not a coincidence, and it’s my favourite fact in this whole topic: every adjunction gives you a monad, and the State monad is the one that currying gives you.

For State, η\eta returns a value without touching the state:

η(a)=λs. (a,s)\eta(a) = \lambda s.\ (a, s)

and chaining runs the first computation, takes the result and the new state, and hands both to the next one. You get the feeling of mutable state while everything stays a pure function.

The mathematics

Definition

Let C\mathcal{C} be a category; for programmers, think of it as “types and functions”. A monad on C\mathcal{C} is a triple (T,η,μ)(T, \eta, \mu) where

  • T:C→CT : \mathcal{C} \to \mathcal{C} is a functor (it sends a type AA to TAT A, and a function f:A→Bf : A \to B to Tf:TA→TBTf : TA \to TB; this is map),
  • η:Id⇒T\eta : \text{Id} \Rightarrow T is a natural transformation called the unit, with components ηA:A→TA\eta_A : A \to TA,
  • μ:T2⇒T\mu : T^2 \Rightarrow T is a natural transformation called the multiplication, with components μA:TTA→TA\mu_A : TTA \to TA (this is flatten or join).

They have to satisfy three laws. Associativity:

μA∘TμA  =  μA∘μTA:TTTA→TA\mu_A \circ T\mu_A \;=\; \mu_A \circ \mu_{TA} \quad : TTTA \to TA

and the two unit laws:

μA∘TηA  =  idTA  =  μA∘ηTA\mu_A \circ T\eta_A \;=\; \text{id}_{TA} \;=\; \mu_A \circ \eta_{TA}

Read them in words. Associativity says that if you have three layers of context, it doesn’t matter whether you flatten the inner two first or the outer two first. The unit laws say that wrapping something in a trivial layer and then flattening gives you back what you started with, whichever side you add the layer on.

If those look like the laws of a monoid ((xy)z=x(yz)(xy)z = x(yz) and 1x=x=x11x = x = x1), that’s because they are. A monad is a monoid where the elements are functors, “multiplication” is composition of functors, and the unit is the identity functor. Hence the infamous line: a monad is just a monoid in the category of endofunctors. It sounds like a joke, but it’s actually a correct and fairly compact definition.

Checking against Maybe: μ\mu takes Maybe(Maybe A)=(A+1)+1\text{Maybe}(\text{Maybe}\,A) = (A + 1) + 1 and collapses it to A+1A + 1 by sending just(just(a))\text{just}(\text{just}(a)) to just(a)\text{just}(a) and both kinds of nothing\text{nothing} to nothing\text{nothing}. You can check the laws by hand in a minute.

For lists: TAT A is finite lists of AA, η(a)=[a]\eta(a) = [a], and μ\mu concatenates a list of lists. Associativity of μ\mu is just associativity of concatenation.

The Kleisli category

Here’s the first big idea. In programming, we rarely work with μ\mu directly. We work with functions of the form

f:A→TBf : A \to TB

“functions that return a value in context”. Kleisli’s insight was that these functions form a category of their own, where the context is handled automatically.

The Kleisli category CT\mathcal{C}_T has:

  • the same objects as C\mathcal{C},
  • a morphism A→BA \to B in CT\mathcal{C}_T is a morphism A→TBA \to TB in C\mathcal{C},
  • the identity on AA is ηA:A→TA\eta_A : A \to TA,
  • composition of f:A→TBf : A \to TB and g:B→TCg : B \to TC is
g∘Tf  =  μC∘Tg∘f:A→TCg \circ_T f \;=\; \mu_C \circ Tg \circ f \quad : A \to TC

Read the composition right to left: run ff to get a TBTB; map gg over it to get a TTCTTC; flatten with μ\mu to get a TCTC. That’s exactly the “chain these context-returning functions” operation from the Maybe example, now stated once for every monad. People write it as g< ⁣= ⁣<fg \mathrel{<\!=\!<} f, or f> ⁣= ⁣>gf \mathrel{>\!=\!>} g in the other order, and call it the fish operator.

The monad laws are exactly what make this a category. Associativity of μ\mu gives associativity of ∘T\circ_T, and the unit laws make η\eta an identity:

ηB∘Tf=f,f∘TηA=f,h∘T(g∘Tf)=(h∘Tg)∘Tf\eta_B \circ_T f = f, \qquad f \circ_T \eta_A = f, \qquad h \circ_T (g \circ_T f) = (h \circ_T g) \circ_T f

This is, I think, the cleanest way to say what monads give programmers: a monad turns effectful functions into something you can compose like ordinary functions, with the same laws. That’s all. The rest is notation.

Kleisli also showed how this answers the 1965 question. There’s a functor FT:C→CTF_T : \mathcal{C} \to \mathcal{C}_T that sends a plain function ff to η∘f\eta \circ f, and a functor UT:CT→CU_T : \mathcal{C}_T \to \mathcal{C} that sends AA to TATA. These are adjoint, FT⊣UTF_T \dashv U_T, and the monad you get back from them is the original TT.

Monad algebras

The second construction asks a different question. Instead of “what are the functions in context?”, it asks: what does it mean to be able to evaluate a context?

An algebra for the monad TT (often called a TT-algebra) is an object AA together with a morphism

a:TA→Aa : TA \to A

that collapses the context, in a way that respects the monad:

a∘ηA=idA,a∘Ta=a∘μAa \circ \eta_A = \text{id}_A, \qquad a \circ Ta = a \circ \mu_A

The first law says: if I wrap a value trivially and then evaluate, I get the value back. The second says: if I have two layers, I can evaluate the inner layer then the outer, or flatten first and evaluate once, and I get the same answer.

This is easiest to feel with the list monad. A list-algebra is a set AA with a map a:List A→Aa : \text{List}\,A \to A that turns any list of elements into a single element, consistently. Take A=NA = \mathbb{N} and a=suma = \text{sum}:

sum([n])=n,sum([sum(xs1),…,sum(xsk)])=sum(xs1+ ⁣ ⁣+⋯+ ⁣ ⁣+xsk)\text{sum}([n]) = n, \qquad \text{sum}([\text{sum}(xs_1), \dots, \text{sum}(xs_k)]) = \text{sum}(xs_1 \mathbin{+\!\!+} \cdots \mathbin{+\!\!+} xs_k)

Both laws hold. In fact, list-algebras are exactly monoids: give me a monoid and folding with its operation is an algebra; give me an algebra and x⋅y=a([x,y])x \cdot y = a([x, y]) with unit a([ ])a([\,]) is a monoid. Similarly, Maybe-algebras are pointed sets: the algebra has to say where nothing\text{nothing} goes, and that’s just picking a default element. If you’ve ever written “value or default”, you’ve written a Maybe-algebra.

A morphism of algebras h:(A,a)→(B,b)h : (A, a) \to (B, b) is a function h:A→Bh : A \to B that commutes with evaluation:

h∘a=b∘Thh \circ a = b \circ Th

For lists, that’s a monoid homomorphism.

The Eilenberg–Moore category

The algebras and their morphisms form a category, written CT\mathcal{C}^T and called the Eilenberg–Moore category. Like Kleisli’s, it comes with an adjunction:

FT:C→CT,A↦(TA,μA)F^T : \mathcal{C} \to \mathcal{C}^T, \quad A \mapsto (TA, \mu_A) UT:CT→C,(A,a)↦AU^T : \mathcal{C}^T \to \mathcal{C}, \quad (A, a) \mapsto A

UTU^T forgets the algebra structure. FTF^T sends AA to the free algebra on AA, which is TATA evaluated by flattening. For lists, the free algebra on AA is the free monoid on AA, which is just lists of AA under concatenation. Again FT⊣UTF^T \dashv U^T, and again the monad you get back is TT.

So we have two adjunctions that both produce TT, and here is the beautiful part. Among all adjunctions that give rise to TT, Kleisli’s is the smallest (it’s the initial one) and Eilenberg–Moore’s is the largest (it’s the terminal one). Every other adjunction producing TT sits between them. And the Kleisli category sits inside the Eilenberg–Moore one: it’s equivalent to the full subcategory of free algebras.

The way I think about it:

  • the Kleisli category is about computations: arrows A→TBA \to TB, things that produce effects,
  • the Eilenberg–Moore category is about handlers: maps TA→ATA \to A, things that consume effects and give you a plain answer.

That split is exactly how modern effect systems and algebraic effects are designed: a program is a Kleisli arrow, a handler is an algebra. Fifty-year-old mathematics, sitting quietly underneath.

Bind

Now back to code. Programmers rarely write μ\mu or Kleisli composition directly. They write bind, usually spelled ≫ ⁣=\gg\!=:

(≫ ⁣=):TA→(A→TB)→TB(\gg\!=) : TA \to (A \to TB) \to TB m≫ ⁣=f  =  μB((Tf)(m))m \gg\!= f \;=\; \mu_B\big((Tf)(m)\big)

Notice the currying again: bind takes a wrapped value and gives back a function that wants a continuation ff. It says: here’s a value in context, and here’s what to do with the plain value once you have it; you figure out the context.

Bind, μ\mu and Kleisli composition are all interdefinable, so a monad can be given by any of them. From bind you recover the others:

μ(mm)=mm≫ ⁣=id,Tf(m)=m≫ ⁣=(η∘f),(f> ⁣= ⁣>g)(a)=f(a)≫ ⁣=g\mu(mm) = mm \gg\!= \text{id}, \qquad Tf(m) = m \gg\!= (\eta \circ f), \qquad (f \mathrel{>\!=\!>} g)(a) = f(a) \gg\!= g

And the monad laws, rewritten in terms of bind, become the three laws you’ll see in every Haskell tutorial:

η(a)≫ ⁣=f  =  f(a)(left identity)\eta(a) \gg\!= f \;=\; f(a) \qquad \text{(left identity)} m≫ ⁣=η  =  m(right identity)m \gg\!= \eta \;=\; m \qquad \text{(right identity)} (m≫ ⁣=f)≫ ⁣=g  =  m≫ ⁣=(λx. f(x)≫ ⁣=g)(associativity)(m \gg\!= f) \gg\!= g \;=\; m \gg\!= (\lambda x.\ f(x) \gg\!= g) \qquad \text{(associativity)}

They’re the same laws as before, just seen from the point of view of someone writing a pipeline. Left and right identity say η\eta doesn’t do anything. Associativity says that how you group the steps of a pipeline doesn’t matter, which is what lets you refactor a long chain into helper functions without changing its meaning.

Bind in TypeScript

TypeScript doesn’t have higher-kinded types, so we can’t write one generic Monad interface the way Haskell does. That’s fine. Writing a couple of concrete monads by hand is more illuminating anyway.

Here’s Maybe:

type Maybe<A> = { kind: 'just'; value: A } | { kind: 'nothing' }

const just = <A>(value: A): Maybe<A> => ({ kind: 'just', value })
const nothing: Maybe<never> = { kind: 'nothing' }

// η
const pure = just

// >>=
const bind = <A, B>(m: Maybe<A>, f: (a: A) => Maybe<B>): Maybe<B> =>
  m.kind === 'nothing' ? m : f(m.value)

That’s the entire monad. Now some functions that can fail:

const parseNumber = (s: string): Maybe<number> => {
  const n = Number(s)
  return Number.isNaN(n) ? nothing : just(n)
}

const reciprocal = (n: number): Maybe<number> =>
  n === 0 ? nothing : just(1 / n)

const safeSqrt = (n: number): Maybe<number> =>
  n < 0 ? nothing : just(Math.sqrt(n))

And chaining them:

const compute = (input: string): Maybe<number> =>
  bind(bind(parseNumber(input), reciprocal), safeSqrt)

compute('4')    // just(0.5)
compute('0')    // nothing   (division by zero)
compute('-4')   // nothing   (negative square root)
compute('hey')  // nothing   (not a number)

No null checks in the pipeline. Each function only deals with its own failure case, and bind handles the plumbing. If you’ve used optional chaining, or the then method on promises, you’ve been using something monad-shaped already. (Promises aren’t a lawful monad, because they flatten nested promises automatically, but they’re close enough that the intuition carries over.)

Here’s State, which is where the currying story pays off:

type State<S, A> = (s: S) => [A, S]

const pureS = <S, A>(a: A): State<S, A> => (s) => [a, s]

const bindS =
  <S, A, B>(m: State<S, A>, f: (a: A) => State<S, B>): State<S, B> =>
  (s) => {
    const [a, s1] = m(s)
    return f(a)(s1)
  }

const get = <S>(): State<S, S> => (s) => [s, s]
const put = <S>(s: S): State<S, void> => () => [undefined, s]

A tiny example: generating fresh variable names, the way a compiler would.

const fresh: State<number, string> = bindS(get<number>(), (n) =>
  bindS(put(n + 1), () => pureS(`x${n}`)),
)

const threeNames = bindS(fresh, (a) =>
  bindS(fresh, (b) =>
    bindS(fresh, (c) => pureS<number, string[]>([a, b, c])),
  ),
)

threeNames(0) // [['x0', 'x1', 'x2'], 3]

There’s no mutable counter anywhere. Every function is pure, and the state is threaded through by bind. The nesting is a bit ugly, though, and that ugliness is exactly what Haskell’s syntax was designed to get rid of.

Monads in Haskell

In Haskell, monad is a type class, and η\eta and bind are its methods:

class Applicative m => Monad m where
  (>>=)  :: m a -> (a -> m b) -> m b
  return :: a -> m a
  return = pure

Compare the type of bind to the math: TA→(A→TB)→TBTA \to (A \to TB) \to TB. It’s the same thing, character for character. The Applicative superclass is there because every monad is also a functor (that’s TT acting on functions) and an applicative (which lets you combine independent computations), and Haskell asks you to provide those too.

Maybe is in the standard library, but here’s what its instance looks like:

instance Monad Maybe where
  Nothing >>= _ = Nothing
  Just a  >>= f = f a

The real treat is do-notation, which is syntactic sugar for chains of bind:

compute :: String -> Maybe Double
compute input = do
  n <- readMaybe input
  r <- reciprocal n
  safeSqrt r

The compiler turns that into

readMaybe input >>= \n -> reciprocal n >>= \r -> safeSqrt r

Each arrow in do-notation is a bind, and the rest of the block is the continuation. It reads like imperative code, but it’s a pure expression, and the monad decides what “next line” means. For Maybe, “next line” means “if we didn’t fail”. For lists, it means “for each possibility”. For State, it means “with the updated state”.

Here’s the fresh-name example again, with a State monad written from scratch:

newtype State s a = State { runState :: s -> (a, s) }

instance Functor (State s) where
  fmap f (State g) = State $ \s -> let (a, s') = g s in (f a, s')

instance Applicative (State s) where
  pure a = State $ \s -> (a, s)
  State f <*> State g = State $ \s ->
    let (h, s1) = f s
        (a, s2) = g s1
    in (h a, s2)

instance Monad (State s) where
  State g >>= f = State $ \s ->
    let (a, s') = g s
    in runState (f a) s'

get :: State s s
get = State $ \s -> (s, s)

put :: s -> State s ()
put s = State $ \_ -> ((), s)

fresh :: State Int String
fresh = do
  n <- get
  put (n + 1)
  return ("x" ++ show n)

threeNames :: State Int [String]
threeNames = do
  a <- fresh
  b <- fresh
  c <- fresh
  return [a, b, c]

-- runState threeNames 0 == (["x0","x1","x2"], 3)

Compare this to the TypeScript version. Same idea, same bind, but the nesting is gone.

And finally, the one that made Haskell take monads seriously: IO. A value of type IO a is a description of a program that, when run, interacts with the world and produces an aa. Bind lets you sequence these descriptions without ever leaving pure code:

main :: IO ()
main = do
  putStrLn "what's your name?"
  name <- getLine
  putStrLn ("hello, " ++ name)

Conceptually, IO is the State monad where the state is the entire outside world. You can’t copy the world or go back in time with it, and the monad structure makes sure you never try.

Further Reading

  1. Eric Lippert’s blog series on monad
  2. Monads by Mark Seeman (this is part of his series of blogs on design patterns and category theory)
  3. Jeremy Bowers’ blog on Monads

footnotes

  1. Godement, R. (1958). Topologie algébrique et théorie des faisceaux. Hermann. ↩

  2. Kleisli, H. (1965). Every standard construction is induced by a pair of adjoint functors. Proceedings of the AMS, 16(3), 544–546. ↩

  3. Eilenberg, S., & Moore, J. C. (1965). Adjoint functors and triples. Illinois Journal of Mathematics, 9(3), 381–398. ↩

  4. Mac Lane, S. (1971). Categories for the Working Mathematician. Springer. ↩

  5. Moggi, E. (1991). Notions of computation and monads. Information and Computation, 93(1), 55–92. ↩

  6. Wadler, P. (1992). The essence of functional programming. POPL ‘92. ↩