A monad is just the thing with flatMap — chain steps where each one depends on the previous result — and Haskell's do block is exactly Scala's for-comprehension wearing different syntax.
You've already used monads for years. Every time you wrote a for-comprehension over an Option, a Future, or a List, you were chaining monadic steps. Haskell pulls that same idea out into a single typeclass with a single defining method — and gives it the same convenient syntax.
Here's the whole chapter, one Scala anchor at a time:
map alone can't do it. You need flatMap.>>=) — the one method that makes a monad, and why it is Scala's flatMap.do-notation — sugar over >>=, exactly like a for-comprehension desugars to flatMap/map.Say you want a user's order. First look up the user by id; then, using that user, look up their order. The second lookup can't even start until the first one hands back a value — and either lookup might come back empty.
Reach for map and you hit a wall. map takes an Option[User] and a function User -> Order. But your second step doesn't return a plain Order — it returns another Option[Order], because it might fail too. Map that and you get an Option[Option[Order]]: a box inside a box.
findUser(id) -- Option[User]
findUser(id).map(findOrder) -- Option[Option[Order]] 😖
You want a flat Option[Order]. The operation that maps and then flattens the nested box in one move is flatMap. That single combinator — chaining a step whose result depends on the previous step's unwrapped value — is the whole idea of a monad.
>>= (bind)Strip away the mystique and a monad is a typeclass with one defining method, pronounced "bind" and spelled >>=:
(>>=) :: m a -> (a -> m b) -> m b
Read it left to right: take a boxed value m a, and a function that — given the unwrapped a — produces a new boxed value m b. Bind feeds the unwrapped result into that function and hands back the new box, flattened. That is Scala's flatMap, argument order flipped:
// Scala
opt.flatMap(x => f(x))
-- Haskell
opt >>= \x -> f x
The other half of the Monad typeclass is return (also called pure): it lifts a plain value into the box — return 5 :: Maybe Int is Just 5. That's Scala's Some(5) / Option(5). Don't let the name fool you: return is an ordinary function, not a control-flow keyword.
There's also >> ("then") — bind that ignores the previous result: a >> b runs a, throws away its value, then runs b. Handy when you only care about the effect, not the value.
Maybe monad — short-circuitBack to the user-then-order chain. With Maybe, bind gives you short-circuiting for free: feed a Just x through and it unwraps x and runs the next step; feed a Nothing through and the rest is skipped — the whole chain is Nothing. No null checks, no nested ifs.
findUser 1 >>= \user ->
findOrder user >>= \order ->
return (total order)
If findUser 1 is Nothing, neither findOrder nor total ever runs — bind on Nothing just returns Nothing and ignores the function. That's exactly how a Scala for-comprehension over Option bails out the moment any step is None.
Pick which of three lookups succeed below, then run the chain and watch the first Nothing halt everything downstream.
Either monad — carry the errorSame shape, more information. Maybe tells you a step failed; Either tells you why. Bind on Either threads the Right (success) value through, and the moment any step returns a Left, that Left carries straight to the end — every later step is skipped.
parseAge "42" >>= \age ->
checkAdult age >>= \ok ->
return ok
-- "bad input" anywhere ⇒ Left "bad input", the rest skipped
This is error handling without exceptions. The failure travels in the return type, the first error wins, and the caller deals with both branches — the same discipline as Scala's Either in a for-comprehension. No try, no stack unwinding, no surprise control flow.
Right; Left is the bail-out lane that short-circuits the chain, error in hand.The list monad is the surprising one, and it's pure Scala for-comprehension. Here bind means "for each": xs >>= f runs f on every element of xs and concatenates all the resulting lists. Chain two binds and you get every combination — this is nondeterminism modelled as data.
[1,2] >>= \x ->
['a','b'] >>= \y ->
return (x, y)
-- => [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
That's identical to a Scala for { x <- xs; y <- ys } yield (x, y) — which is exactly why for-comprehensions over lists feel like nested loops. The "loop" is just bind walking each element. Build two small lists below and watch the pairs branch out.
do-notation — sugar over bindWriting >>= \x -> by hand gets noisy fast. So Haskell gives you do-notation — pure syntactic sugar that reads top to bottom like imperative code, but desugars straight back into >>= and >>. The two rules:
x <- m means "bind m, call the unwrapped value x" — it becomes m >>= \x -> …rest.m (no arrow) sequences with >> — run it, discard the value, continue.This is literally Scala's for-comprehension. x <- m in a do block is x <- m in a for block; the final pure e is the yield e. Flip between the sugar, the raw bind, and the Scala original below — same program, three skins.
The user-then-city lookup, side by side. Scala's for/yield over Option, and Haskell's do/pure over Maybe. Line for line, they're the same chain.
For a type to behave like a monad, return and >>= must obey three laws. You won't recite them daily, but they're why do-notation is safe to refactor:
return a >>= f ≡ f a. Wrapping then binding is just calling.m >>= return ≡ m. Binding the wrapper changes nothing.(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g).And the practical wisdom: don't reach for a monad when your steps are independent. If two computations don't depend on each other's results — validate three form fields, fetch two unrelated values — Applicative (Ch.5) combines them without forcing a sequence, and can collect all the errors instead of stopping at the first. Monads are for genuine dependency; Applicative is for parallel-shaped work. Reach for the weakest tool that does the job.
<*> / mapN — and the clearer, more parallel intent that comes with it.You can now chain dependent steps in any monad and read a do block as the for-comprehension it really is. The biggest monad of all is the one that lets a pure language actually do things — print, read, and run side effects in order. That's IO, and it's just bind with the outside world on the other end.