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:
f andThen g fails when f returns a Some.>=> โ composition in this new category.By the end, your for-comprehensions will look different.
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.
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.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:
a => f(a).flatMap(g). Apply f, then flatMap the result through g. One layer in, one layer out.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.
flatMap), but it's the same recipe.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.
>=> in codeComposition 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].
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.Every monad gives you a Kleisli category. Different monads make for very different kinds of arrows. Three you've already used:
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.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.
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.