โˆ˜ Category Theory for programmers
๐ŸŸ Chapter 7 ยท Where monads finally make sense

Monadic code is composition

Functions that return a Some or a List or a Future don't fit together with the regular composition operator. So why does chaining them feel like ordinary composition? Because there's a second category hiding right next to the first one โ€” and in that category, they compose perfectly.

Chapter 6 left you with a tool โ€” flatMap โ€” and a question you maybe didn't notice. We kept saying "chain these effectful steps together," which sounds an awful lot like composition. But the steps have type A => M[B], not A => B. So plain composition can't actually glue two of them. What's going on?

Here's the road map for this chapter:

  • See exactly why f andThen g fails when f returns a Some.
  • Notice that if we change what an "arrow" means, the composition snaps back into place.
  • Meet the fish operator >=> โ€” composition in this new category.
  • See that the monad laws and the category laws are literally the same equations.
  • Walk through three Kleisli categories you already use every day.

By the end, your for-comprehensions will look different.

1The chain that didn't compose

Back to the opening scene from Chapter 6. You had a function parseInt : String => Option[Int] and an input "42". You tried to .map(parseInt) over it and got Some(Some(42)) โ€” a stack of boxes that grew with every step. flatMap fixed it: one application produced Some(42), flat and clean.

Now zoom out. In Chapter 1 we celebrated composition: given f : A โ†’ B and g : B โ†’ C, the composite g โˆ˜ f : A โ†’ C is right there in the category. But these effectful functions don't have that shape. Each one looks like A โ†’ M[B]. Try to plug two of them into Scala's andThen:

f: String => Option[Int]  andThen  g: Int => Option[Double] // โœ— types don't fit

Why? Because f hands you an Option[Int], but g wants a plain Int. The output of one step isn't the input the next step expects. Plain composition is blocked at the type level โ€” there's an Option wrapper in the way.

And yet, when we wrote for { x <- a; y <- b } yield ..., it felt like the steps were composing. They produced one output and fed it to the next. So something composition-shaped is happening โ€” just not in the obvious category.

Interactive ยท why the lanes look different click "โ–ถ step" to walk both lanes ยท top one fails, bottom one works
two ways to chain ยท same data ยท only one fits the types
๐Ÿ’ก
The top lane is what plain composition would have to do โ€” and it gets stuck the moment a wrapper appears. The bottom lane composes through flatMap, which unwraps as it goes. The same data flows through both, but only the bottom one has the right types. That bottom lane is going to turn out to be composition in a different category.

2A new category, hiding in plain sight

Here's the trick. We don't have to throw out the idea of composition. We just have to change what we mean by "an arrow from A to B."

In the original category โ€” call it the ordinary one โ€” an arrow from A to B is a plain function A โ†’ B. We've been working in it since Chapter 1.

Now sit down a new category right next to it. Same objects (still Int, String, List[User], โ€ฆ) but a different definition of arrow:

an arrow from A to B  =  a function  A โ†’ M[B]

That's it. An "arrow" in this new world is a function that takes a plain A and produces a B wrapped in the context M. The functions we couldn't compose before? They're exactly the arrows of this new category.

For this to be a real category, we need three things: a way to compose two arrows, an identity arrow on each object, and the usual laws. Here's how they shake out:

  • Objects โ€” the same plain types you've always had.
  • Arrows from A to B โ€” functions A โ†’ M[B].
  • Composition โ€” given f : A โ†’ M[B] and g : B โ†’ M[C], define (f >=> g) : A โ†’ M[C] as a => f(a).flatMap(g). Apply f, then flatMap the result through g. One layer in, one layer out.
  • Identity โ€” for each object A, the identity arrow is pure : A โ†’ M[A] (Scala's Some(_), Haskell's return). The "do nothing" arrow โ€” it just lifts the value into the context.

This construction has a name: the Kleisli category of the monad M, often written K(M) or Kleisli(M). There's one for every monad. K(Option), K(List), K(Future), K(IO) โ€” each is its own category, sitting beside the ordinary one, with its own notion of arrow and composition.

๐Ÿ”
Echo from Chapter 1: a category is "objects + arrows + a way to compose them + an identity per object, obeying two laws." We just built one. The arrows look weird (they're effectful functions), and composition looks weird (it uses flatMap), but it's the same recipe.

3Why it's a category โ€” the monad laws were category laws in disguise

Two things still need checking. For our shiny new construction to deserve the name "category," composition with >=> has to be associative, and pure has to actually behave as the identity (gluing it on either end of any arrow changes nothing). Three equations to verify.

Watch what they reduce to.

Side-by-side ยท monad laws โ†” category laws left column is what Ch.6 called the monad laws ยท right column is what they MEAN in K(M)
A category needs three things to hold. Each one is a monad law.
Category law
Monad-law form
Category-law form (in K(M))
Left identity
pure(a).flatMap(f) == f(a)
โ‡”
pure >=> f  ==  f
โœ“
Right identity
m.flatMap(pure) == m
โ‡”
f >=> pure  ==  f
โœ“
Associativity
(m.fM(f)).fM(g) == m.fM(x => f(x).fM(g))
โ‡”
(f >=> g) >=> h  ==  f >=> (g >=> h)
โœ“
๐ŸŽฏ
The punchline. The three monad laws aren't a separate set of arbitrary rules. They are exactly the two category laws (identity, associativity) re-expressed in the language of flatMap and pure. The reason a monad has to satisfy them is the same reason any category does โ€” without them, "composition" is a lie.

4The fish operator: >=> in code

Composition in K(M) has a symbol. Haskell calls it the fish operator because >=> looks like one if you squint.

(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)

Read the type literally: "take an effectful step a -> m b, take a second effectful step b -> m c, and produce a single effectful step a -> m c that chains them." That's it. That's composition, just with m wrappers along the way.

The Haskell definition is one line:

f >=> g  =  \a -> f a >>= g

"Apply f to get a wrapped result, then >>= (flatMap / bind) it through g." Scala doesn't ship >=> in its standard library โ€” but cats does, on its Kleisli type. Or write it yourself in three lines.

Build a chain step by step below. Each new step extends the composite, and you can see the type stay clean the whole way: A โ†’ M[B].

Interactive ยท build a fish-chain click "add step" to extend the composite ยท watch the type
a chain of arrows in K(Option)
โš ๏ธ
Order gotcha (same as Ch.1). Haskell's f >=> g runs f first, then g โ€” "left to right," like Scala's andThen. There's also a right-to-left version called <=< if you want the visual to match โˆ˜. Pick a direction and stick with it.

5Three Kleisli categories you already know

Every monad gives you a Kleisli category. Different monads make for very different kinds of arrows. Three you've already used:

K(Option)
A โ†’ Option[B]
Partial functions. An arrow may return None and refuse to produce an output. Composition short-circuits: the first None in the chain becomes the chain's final answer. This is "what if it's not there" without exceptions.
K(List)
A โ†’ List[B]
Nondeterministic functions. An arrow may return many possible outputs. Composition explores every path: for each result of the first arrow, run the second arrow on it, and collect everything. The cartesian-product effect from Chapter 6, expressed as a category.
K(Future) ยท K(IO)
A โ†’ Future[B]  / A โ†’ IO B
Effectful functions. Each arrow sequences a real-world effect (network, time, I/O). Composition runs them in order, each step waiting for the previous one. This is the category Haskell's whole I/O system lives in.
๐Ÿงญ
Every chapter from here on lives inside one of these categories โ€” or one like them. The reason "effectful programming" feels like a coherent discipline rather than a pile of patterns is that it is coherent: you're navigating a category, with the same composition laws you learned in Chapter 1, just relabelled.

6The same idea, in code

The fish operator written out in both languages, plus a small two-step chain โ€” parse a string to an Int, then take its square root if it's positive โ€” composed three different ways: directly, with >=>, and as sugar.

7The payoff

Pull the camera back. Every for-comprehension you've ever written, every Haskell do-block, every cats Kleisli[F, A, B] โ€” that's all the same thing: arrows being composed inside a Kleisli category. The effects (failure, nondeterminism, async, I/O) become invisible because the category absorbs them into its notion of arrow. You just compose.

The reason monadic code feels like ordinary function composition is because it is ordinary function composition โ€” you've just moved into a different category where "function" and "compose" mean slightly different things. The laws are the same. The intuition is the same. Only the wrapping changed.

Programmer take-away: when you reach for Kleisli[F, A, B] in cats (or ReaderT-style stacks in Haskell), you're building a value that is an arrow in some K(F). They compose with andThen, they have identities, and they obey the laws because the underlying monad does. Reach for them anytime you want to compose effectful steps as cleanly as you compose ordinary functions.

8Check yourself

3 questions ยท instant feedback 0 / 3