Effects and Monads
The pure lambda calculus
is a world of values and functions with no side effects: nothing prints, nothing throws,
nothing reads a mutable cell, nothing fails. Real programs do all of these things. The central puzzle
of typed functional programming is how to keep the equational, referentially-transparent
core — where e and its value are interchangeable everywhere — while still
describing computations that print, fail, or thread state. The answer that reshaped the field
is the monad: a single, astonishingly reusable interface that turns "a computation
that may also do X" into an ordinary, first-class value.
A monad is not magic and not (originally) about I/O. It is an algebraic structure — lifted straight
from category theory by Eugenio Moggi in 1989 to model effects in a denotational semantics,
and then turned by Philip Wadler into a programming discipline. Once you see the shape, you
find it everywhere: Maybe for failure, State for mutable stores,
List for nondeterminism, Reader for an environment, IO for the
outside world. All of them share the same two operations and obey the same three laws.
Moggi's idea: separate values from computations
Moggi's move is to distinguish a value of type A from a
computation that produces an A while possibly doing some effect.
He writes the type of such a computation as T\,A, where
T is a type constructor capturing "the effect". A
function that can fail is not A \to B but
A \to T\,B — a Kleisli arrow. The whole trick is then: how do
we compose two effectful steps A \to T\,B and
B \to T\,C into one A \to T\,C?
A monad on a language is a type constructor T with two operations:
-
\mathsf{return} : A \to T\,A (also called
unit, \eta) — inject a pure value as a trivial computation
that does no effect.
-
(\mathbin{>\!\!>\!\!=}) : T\,A \to (A \to T\,B) \to T\,B
(bind) — run a computation, feed its result to the next effectful step, and stitch the two
effects together in order.
Read m \mathbin{>\!\!>\!\!=} f as "do m, call its
result x, then do f\,x." Sequencing — the thing
imperative languages bake into the semicolon — becomes an ordinary higher-order function.
Kleisli composition, drawn
Bind lets us compose Kleisli arrows just like ordinary functions compose. Given
f : A \to T\,B and g : B \to T\,C, their
Kleisli composition f >\!=> g : A \to T\,C is
\lambda a.\; f\,a \mathbin{>\!\!>\!\!=} g. The effects of
f happen, then the effects of g, and the
T wrapper carries the accumulated effect through untouched:
The top row is the "pure" world of plain values; each downward step wraps a result inside
T. \mathsf{return} is the little arrow that drops
a pure value into T for free, and >\!\!>\!\!= is
the machine that unwraps the left computation, hands its value to the next arrow, and re-wraps the
combined effect.
The three monad laws
An interface is only trustworthy if it obeys laws — otherwise refactoring changes behaviour. A monad
must satisfy three equations, which say exactly that \mathsf{return} is a
neutral step and that sequencing is associative:
\mathsf{return}\,a \mathbin{>\!\!>\!\!=} f \;\equiv\; f\,a \qquad\text{(left identity)}
m \mathbin{>\!\!>\!\!=} \mathsf{return} \;\equiv\; m \qquad\text{(right identity)}
(m \mathbin{>\!\!>\!\!=} f) \mathbin{>\!\!>\!\!=} g \;\equiv\; m \mathbin{>\!\!>\!\!=} (\lambda x.\; f\,x \mathbin{>\!\!>\!\!=} g)
In Kleisli-composition terms these are precisely "\mathsf{return} is the
identity arrow, and >\!=> is associative" — the axioms of a
category. That is the whole content of "a monad is a monoid in the category of endofunctors":
an unhelpful slogan hiding a very helpful fact.
Two monads, one interface, running
Below, Maybe (failure) and State (a threaded integer store) are two utterly
different effects — yet both are just a return and a bind. Once defined, the
same sequencing code reads like straight-line imperative programming, but stays pure. Run it:
// ----- Maybe: computations that may fail -----
type Maybe<A> = { tag: "none" } | { tag: "some"; value: A };
const none: Maybe<never> = { tag: "none" };
const some = <A>(value: A): Maybe<A> => ({ tag: "some", value });
const retMaybe = some; // return
function bindMaybe<A, B>(m: Maybe<A>, f: (a: A) => Maybe<B>): Maybe<B> {
return m.tag === "none" ? none : f(m.value); // short-circuit on failure
}
const safeDiv = (x: number, y: number): Maybe<number> =>
y === 0 ? none : some(x / y);
// (100 / 5) / 2 / ... — failure anywhere collapses the whole chain, no if-nesting.
const r1 = bindMaybe(safeDiv(100, 5), (a) =>
bindMaybe(safeDiv(a, 2), (b) =>
retMaybe(b + 1)));
const r2 = bindMaybe(safeDiv(100, 0), (a) => // divides by zero...
bindMaybe(safeDiv(a, 2), (b) =>
retMaybe(b + 1))); // ...and everything after is skipped
console.log("Maybe:", JSON.stringify(r1), "then", JSON.stringify(r2));
// ----- State: a computation threading an integer store -----
type State<A> = (s: number) => [A, number]; // s -> (result, s')
const retState = <A>(a: A): State<A> => (s) => [a, s];
function bindState<A, B>(m: State<A>, f: (a: A) => State<B>): State<B> {
return (s) => { const [a, s1] = m(s); return f(a)(s1); }; // thread s through, in order
}
const get: State<number> = (s) => [s, s];
const put = (n: number): State<null> => (_s) => [null, n];
const tick: State<number> = bindState(get, (n) => bindState(put(n + 1), () => retState(n)));
// Run tick three times starting from store = 10; each yields the old value and bumps the store.
const prog = bindState(tick, (a) => bindState(tick, (b) => bindState(tick, (c) => retState([a, b, c]))));
console.log("State:", JSON.stringify(prog(10))); // [[10,11,12], 13]
Notice the payoff: the plumbing of each effect — short-circuiting for Maybe,
threading the store for State — is written once inside bind,
and every program built on top is freed from writing it by hand. That is exactly what a language's
do-notation (Haskell) or computation expressions (F#) desugar into.
Beyond monads: effect systems and algebraic effects
Monads have a well-known weakness: they don't compose. Combining State and
Maybe and IO forces you to stack monad transformers, and the
lifting boilerplate grows with the tower. Two research directions answer this:
-
Effect systems annotate a function's type with the set of effects it may
perform — a judgement \Gamma \vdash e : \tau \,!\, \varepsilon where
\varepsilon is a row of effects (reads, writes, exceptions). The type
checker then tracks and enforces purity where it matters.
-
Algebraic effects and handlers (Plotkin & Pretnar) split an effect into
operations (like
get, raise) and a separately-installed
handler that interprets them — much like resumable exceptions. They compose freely and are
now in languages such as Koka, Eff, OCaml 5 and Unison.
Both keep Moggi's founding insight: an effect is data about a computation that the type system
can see and reason about, not an invisible thing that happens behind the semantics' back.
Monads arrived in programming by an unlikely route. Eugenio Moggi was building a
denotational semantics and noticed that many different effects — partiality, state,
exceptions, continuations, nondeterminism — all fit the same categorical pattern of a
strong monad. He used it purely as a notation for the semantics. Philip
Wadler then realised the same structure could be a programming abstraction, and the lazy
language Haskell adopted it to tame I/O without giving up referential transparency — famously letting
main :: IO () describe the whole interaction with the world as one pure value that the
runtime executes. So the "scary" category theory is really the receipt proving that a very practical
idea — "sequence these effectful steps" — is sound and refactor-safe.
The single most common misconception is that a monad is "a wrapper type with a
flatMap/bind method". The operations are necessary but not
sufficient: a type only is a monad if return and bind also
satisfy the three laws (left identity, right identity, associativity). A structure with a plausibly-typed
bind that breaks associativity — some "promise" and event-stream types have historically
done this — is not a lawful monad, and code that relies on monadic reasoning (like reordering
or refactoring do-blocks) will silently change meaning. Conversely, a monad is not
inherently about I/O or impurity: Maybe, List and Reader are
perfectly pure monads. The laws, not the vibe, define the concept.