∘ Category Theory for programmers
πŸͺ Chapter 6 Β· The payoff β€” chaining when context follows along

Monads, one chain at a time

When each step of your pipeline might add its own box β€” Option, List, Future β€” naive chaining buries the answer under layers of wrapping. flatMap is the move that collapses those layers as you go. That's the whole story.

You already know functors from Chapter 4. They give you map β€” a way to apply a plain function inside a box without unwrapping it. Today's chapter is about what happens when the function you're applying also returns a box. That sounds like a small wrinkle. It is not. It's the whole reason monads exist.

Here's the road map:

  • See the problem: map alone keeps stacking boxes inside boxes.
  • Meet flatMap β€” the move that collapses one layer per step.
  • Watch it short-circuit (Option) and explore combinations (List).
  • Check the three laws that make all of this safe.
  • Decode what for and do actually do under the hood.

By the end you'll have a name for a pattern you've used a hundred times β€” and you'll see it everywhere from now on.

1The problem: map keeps nesting

Imagine a function parseInt: String => Option[Int]. It might fail, so it returns an Option. Standard stuff. Now you have a Some("42") and you want to parse it.

What does Some("42").map(parseInt) give you? Trace the types: map takes String => X and returns Option[X]. Here X is Option[Int]. So the result is Option[Option[Int]] β€” specifically Some(Some(42)).

One step in and the answer is already wrapped twice. Do it again and you've got Some(Some(Some(...))). The chain grows a layer per call. Watch:

Interactive Β· nesting vs flatMap click "next step" on the left card and watch the type explode
Using map
Some(5)
Option[Int]
0 steps applied
Using flatMap
Some(5)
Option[Int]
0 steps applied
same logical chain, two very different types
πŸ’‘
The function we're applying each step has type Int => Option[Int] (specifically x => Some(x+1)). With map, the outer box doesn't know that the result is itself a box β€” so it just wraps it. With flatMap, the outer box unwraps the inner result before re-boxing. One layer in, one layer out.

2flatMap: the move

The fix is one method. Read its name literally: flatMap is map followed by flatten. It applies the function (which returns a box), then peels off the extra wrapping it just created. So:

Some(5).flatMap(x => Some(x+1))  ==  Some(6)

Not Some(Some(6)). Some(6). The result type matches the function's return type. You can do this all day and stay at one layer.

That's enough to graduate from "functor" to a new word. A type with two operations β€” a way to lift a plain value into the box (call it pure, or in Scala just Some(_) / in Haskell return), and a working flatMap β€” is what people call a monad.

⚠️
"Monad" gets used in a lot of intimidating sentences. Ignore those. For our purposes: a monad is a context with pure and flatMap that obeys three small laws. Option, List, Future, Either, IO β€” all monads, all the same shape.

3Option as a monad: short-circuiting

Here's where Option's monad-ness earns its keep. A clean chain of Somes passes the value along. The moment one step returns None, every step after it is skipped automatically β€” the chain still produces a single None at the end. No exception handling, no early-return scaffolding, no if (x != null) ladders.

Try it. Click the steps one by one. Then use the toggle on any step to swap it for a returning-None version and watch the rest of the chain go dark.

Interactive Β· Option flatMap chain click each step in order Β· toggle "None?" on any step to break the chain
starting value: Some(5)
πŸ’‘
None is the circuit breaker. Once it appears, every subsequent flatMap trivially produces None and never even invokes its lambda. This is the same shape as Scala's Try, Either, and Future: an "abnormal" branch that short-circuits the rest of the pipeline for free.

4List as a monad: every combination

Different box, same machinery, completely different feel. When you flatMap over a List, the function you pass returns another list β€” and flatMap concatenates all those lists together.

The natural consequence: xs.flatMap(x => ys.map(y => (x, y))) visits every x, and inside each one visits every y, collecting all the pairs. That's the cartesian product. List's monad is "explore every combination."

Interactive Β· cartesian product pick two small lists Β· click flatMap Β· watch the pairs appear
xs = ys =
val pairs = xs.flatMap(x => ys.map(y => (x, y)))
0 pairs

5The three laws (last time, promise)

Just like composition had two laws in Chapter 1, flatMap and pure have three. They're what makes for-comprehensions and do-notation safe to use β€” and what makes one monad behave like every other monad.

Each tab below runs the law on real values. Click through them.

Interactive Β· monad laws on concrete values
Wrapping then chaining = just applying. Lifting a value into the box with pure and immediately flatMap-ing should give exactly the same thing as just applying the function. pure shouldn't do anything extra.
pure(5).flatMap(f) where f = x => Some(x + 1) = Some(5).flatMap(x => Some(x + 1)) = Some(6)
≑
f(5) = (x => Some(x + 1))(5) = Some(6)
βœ“ both sides equal Some(6)
FlatMapping with pure changes nothing. If you chain a step that does nothing but re-wrap the value, you get back exactly what you started with. pure is the unit element of flatMap β€” same role id plays for composition.
m.flatMap(pure) where m = Some(5) = Some(5).flatMap(x => Some(x)) = Some(5)
≑
m = Some(5)
βœ“ both sides equal Some(5)
Grouping the chain doesn't change the result. Same melody as Chapter 1's associativity for composition. Whether you collapse the first two steps first or the last two β€” same answer. This is what makes long for-comprehensions equivalent to nested ones, and what lets compilers refactor them freely.
(m.flatMap(f)).flatMap(g) m = Some(5), f = +1, g = Β·2 = Some(6).flatMap(g) = Some(12)
≑
m.flatMap(x => f(x).flatMap(g)) = Some(5).flatMap(x => Some(x+1).flatMap(g)) = Some(5).flatMap(x => Some((x+1)Β·2)) = Some(12)
βœ“ both sides equal Some(12)
πŸ”
Echo from Chapter 1: associativity is the law that lets you stop worrying about parentheses. There, it was (h ∘ g) ∘ f = h ∘ (g ∘ f). Here, it's the same idea β€” chained flatMaps can be regrouped without changing the result.

6for / do: the syntax that hides it

Scala's for-comprehensions and Haskell's do-notation look like imperative code. They aren't. They're sugar β€” a friendlier face for a chain of flatMaps ending in a map. Now that you've seen the chain, the sugar should look harmless.

Hit the button to see the desugaring.

Interactive Β· what for really is
What you write (Scala)
for {
  x <- aOpt
  y <- bOpt
} yield x + y
last line is always map, not flatMap
πŸ’‘
The pattern is "every x <- m becomes m.flatMap(x => ...), except the very last one combined with yield, which becomes .map(x => ...)." Haskell's do { x <- m; ... } sugars to the same shape using >>=.

7The same idea, in code

Side by side. Scala spells it flatMap + for. Haskell spells it >>= ("bind") + do. Same shapes, same laws, same payoff.

8The payoff

Why does any of this matter? Because every meaningful computation a real program does has some kind of context tagging along β€” and once you can flatMap through that context, you stop writing the same plumbing over and over.

The shape "chain a sequence of context-aware computations, where each step might add more context" appears everywhere:

  • Optional values β€” Option, Maybe: missingness short-circuits.
  • Failure with detail β€” Either, Try: the first error wins.
  • Nondeterminism β€” List: every combination, like a nested for-loop.
  • Async work β€” Future, IO: each step waits for the previous one.
  • Mutable-looking state, threaded purely β€” State.
  • Parsing β€” combinator libraries are monads to the bone.
  • Side effects in pure code β€” Haskell's IO.

Once you start seeing flatMap, you see it everywhere. That's the gift this whole series has been winding toward.

9Check yourself

3 questions Β· instant feedback 0 / 3

∎End of Part 1

From a "do nothing" arrow on a single object all the way to monads. Six chapters, one through-line: composition. Every concept has been a refinement of that single move.

Composition (Ch.1) gave you the engine. Types & functions (Ch.2) named the things you compose. Products & coproducts (Ch.3) gave you the algebra of bundling and choice. Functors (Ch.4) lifted composition into contexts. Natural transformations (Ch.5) gave you a principled way to translate between contexts. And monads (Ch.6) gave you the sequencing primitive for context-aware programs.

You now have the programmer-facing core of category theory. Part 2 picks up where this ends β€” starting with the secret category hiding inside every monad.

🐟
Up next Β· Chapter 7
Kleisli Categories
Where monads finally make sense. The secret reason monadic code feels like ordinary composition β€” it IS, just in a different category.
β†’