Monads, from motivic spectra to functional programming
Monads, from motivic spectra to functional programming
Monads come from two completely different worlds depending on who you ask. Mathematicians get them from adjunctions. Functional programmers get them from a typeclass with pure and bind. The shared name is no accident, and the bridge between the two views is the Kleisli category. Once you see it, the translation between composing effects in code and composing adjoints feels obvious.
I spent my PhD doing motivic homotopy theory, where adjunctions are everywhere. Now I write Lean 4 most days (it's my favourite FP language, which is why every code snippet below is in Lean), and a do block reads to me as a chain of unit and counit calculations. This post is partly notes to myself about why those two pictures are the same picture.
The definition
A monad on a category is an endofunctor with two natural transformations, a unit and a multiplication,
satisfying associativity and unit laws. Nothing here forces you to think of as a computational effect. It is an endofunctor with monoid-like structure, full stop.
The main source of monads in math is adjunctions. Given with and , you get a monad on for free. The unit is the unit of the adjunction. The multiplication is , where is the counit.
Where this lives in algebraic geometry
If you work in algebraic geometry or motivic homotopy theory, adjunctions are not exotic. They are how you compute.
Take an open inclusion of schemes. On sheaves or spectra you have , and the induced monad on is . It sends a sheaf to the sheaf you get by restricting to and pushing back out, forgetting whatever did outside . The unit is the restriction map. Since is an open immersion, the counit is an isomorphism, so is too. The monad is idempotent. Localizing twice at is the same as localizing once.
Things get more interesting in the six-functor formalism on -categories of motivic spectra . For a structural morphism , the adjunction gives a monad on , and this monad encodes the cohomology of . Evaluating on the motivic sphere spectrum gives , the spectrum representing cohomology of over .
The piece I find most beautiful is what the multiplication does. The map is induced by the counit , and when you apply it to these spectra, the map is the cup product in cohomology. The unit is the inclusion of constants. None of this looks remotely like a programming construct, but the categorical pattern is identical to how software engineers thread state and I/O.
The Kleisli category
To get from one world to the other you need the Kleisli construction. For a monad , the Kleisli category has the same objects as . A morphism in is a morphism in .
The identity at in is . Composition of and goes through and collapses via :
For the motivic cohomology monad above, Kleisli composition is pulling back, pushing forward, and gluing with the cup product. For a programmer, Kleisli composition is sequencing effectful operations. The morphisms are the same shape.
Lean 4
In Lean 4 a monad is a typeclass:
class Monad (m : Type u → Type v) where
pure : α → m α
bind : m α → (α → m β) → m β
The dictionary is direct.
pureis .bind, often written>>=, is Kleisli composition evaluated at a point.
When you write a do block,
do
let x ← e1
let y ← e2 x
pure (x + y)
it desugars into a chain of continuations:
e1 >>= fun x =>
e2 x >>= fun y =>
pure (x + y)
Every line is a morphism . The do notation is sugar over a long chain of Kleisli compositions. The associativity of bind is the associativity of the Kleisli category, restated as a monad law.
The state monad as an adjunction
The classic example. Fix a set of states. In you have the tensor-hom adjunction
with the right adjoint sending to . The induced monad on is
the type of a stateful computation returning a value in . The unit sends to the constant function . The multiplication comes from the counit of the adjunction, the evaluation map given by .
In Lean 4:
def State (σ α : Type) := σ → α × σ
instance : Monad (State σ) where
pure a := fun s => (a, s)
bind x f := fun s =>
let (a, s') := x s
f a s'
The body of bind is working. You run the first computation to get an updated state, then feed the new state into the next. Threading state by hand is what does for you in the abstract.
Free monads and syntax
Most everyday FP monads come from less geometric, free-forgetful adjunctions. Option, the functor , comes from the free pointed set adjunction. List comes from the free monoid adjunction. In each case is the composite of free and forgetful functors.
A cleaner way to see the same thing is through polynomial functors. A polynomial functor is a signature: operations with input arities indexed by . The free monad satisfies
so is the set of finite formal syntax trees built from your operations, with leaves of type .
For , terms are binary trees . The free magma. Quotient by associativity and you pass from the free monad to the category of Eilenberg-Moore algebras, getting semigroups.
In math, a free monad is pure formal syntax, and an algebra over it is evaluating the syntax subject to laws. In functional programming the same idea has a name, the Free Monad pattern. You define your program's effects as a polynomial functor, then write an interpreter (an Eilenberg-Moore algebra) to give those effects meaning.
Why both sides agree
So why does the same pattern govern side effects in Haskell and stable motivic homotopy theory? Because adjunctions are everywhere, and every adjunction comes with a monad for free.
The interesting asymmetry is which side you start from. In math, you reach for the adjunction first, pullback against pushforward, left adjoint against right adjoint, and the monad shows up as bookkeeping for operations like the cup product. In FP, you reach for the monad first, and the adjunction is hidden behind the typeclass.
A programmer threading state through a do block is doing the same categorical motion as a geometer pushing through a unit and a counit. Different vocabularies, same algebra.