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:

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:

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.