Monads
Forget burritos. Forget warehouses. A Monad is a typeclass with two operations and three laws, and the only thing it actually adds over Applicative is the ability for a later step to depend on the value produced by an earlier step.
That sentence does the work of every metaphor anyone has ever invented. The rest is examples.
The typeclass
class Applicative m => Monad m where
return :: a -> m a -- same as pure; legacy name
(>>=) :: m a -> (a -> m b) -> m b
>>= is pronounced "bind". It takes a wrapped value m a and a function that, given the unwrapped a, produces a new wrapped value m b. The shape of "given the unwrapped a" is exactly what Applicative cannot do.
With Applicative, the second action is fixed before the first runs:
(+) <$> readLn <*> readLn -- two reads no matter what
With Monad, the second action can be chosen based on the first:
do
n <- readLn
if n < 0 then pure 0 else readLn
That is the entire conceptual delta. Everything else — do-notation, monad transformers, MTL — is plumbing built on top.
What >>= does for specific types
This is where it stops being abstract.
Maybe: early exit
instance Monad Maybe where
Nothing >>= _ = Nothing
Just x >>= f = f x
If the value is missing, the rest of the computation is skipped. If it is there, feed it to the next step:
lookup :: Eq k => k -> [(k, v)] -> Maybe v
lookupRoute :: String -> Maybe Int
lookupRoute name = do
user <- lookup name users -- Maybe User
addr <- lookup user addresses -- Maybe Address
pure (zipCode addr) -- Maybe Int
Three sequential lookups, each depending on the previous. Any Nothing short-circuits the whole chain. Without monadic do-notation you would be writing nested case expressions:
lookupRoute name =
case lookup name users of
Nothing -> Nothing
Just user -> case lookup user addresses of
Nothing -> Nothing
Just addr -> Just (zipCode addr)
That is the value of >>= for Maybe: it collapses nested case-on-failure into a flat sequence.
Either: early exit with a reason
instance Monad (Either e) where
Left e >>= _ = Left e
Right x >>= f = f x
Same shape as Maybe, but failure carries information:
data ParseError = NotANumber String | OutOfRange Int
parsePort :: String -> Either ParseError Int
parsePort s = do
n <- maybe (Left (NotANumber s)) Right (readMaybe s)
if n >= 1 && n <= 65535
then Right n
else Left (OutOfRange n)
The first failure wins, and you know what failed. This is the standard pattern for expected, recoverable errors in production Haskell — Mercury's banking codebase, the IOG/Cardano node, hasura's GraphQL engine all lean on Either-shaped monads (often via ExceptT) for domain errors.
List: nondeterminism
instance Monad [] where
xs >>= f = concatMap f xs
Each element produces a list; results are concatenated. This is "try all the possibilities":
pairs :: [(Int, Int)]
pairs = do
x <- [1, 2, 3]
y <- [10, 20]
pure (x, y)
-- [(1,10),(1,20),(2,10),(2,20),(3,10),(3,20)]
Pythagorean triples in three lines:
triples :: Int -> [(Int, Int, Int)]
triples n = do
a <- [1..n]
b <- [a..n]
c <- [b..n]
guard (a*a + b*b == c*c)
pure (a, b, c)
Each <- "picks" a value, and the do-block is run for every combination. guard from Control.Monad prunes branches that fail a predicate. This is the same idea logic programmers use in Prolog, but mostly typed and free of cuts.
IO: real-world effects
main :: IO ()
main = do
putStr "Name: "
name <- getLine
putStrLn ("Hello, " ++ name)
getLine :: IO String. <- runs the action and binds the result. Each step sequences after the previous one — that ordering is what makes IO sane. Desugared, it is:
main =
putStr "Name: " >>= \_ ->
getLine >>= \name ->
putStrLn ("Hello, " ++ name)
The >>= is the same operator. The behaviour is type-specific: for IO, it means "run the first action, then run the function on its result".
Do-notation is sugar
Every do-block desugars to >>= and >>:
do x <- m; rest === m >>= \x -> do rest
do m; rest === m >> do rest
do let x = e; rest === let x = e in do rest
do m === m
That is the entire transformation. Once you see it, you can read any do-block as a chain of binds. The advantage of do-notation is that nested binds get unbearable past three or four steps; the disadvantage is that beginners think do-notation is special syntax for "imperative Haskell". It is not. It is >>= rearranged.
The laws
Three laws. Like the Functor laws, the compiler does not check them; library authors and users assume them.
Left identity
return x >>= f == f x
Wrapping a value and immediately binding is the same as just calling the function.
Right identity
m >>= return == m
Binding to return does nothing.
Associativity
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
You can re-associate binds without changing meaning. This is what makes do-notation safe to refactor — pulling a sub-block into a helper does not change behaviour.
If a Monad instance breaks these, do-notation lies to you. The IO and Maybe and Either e instances are all lawful. So is the State monad. Some clever experimental monads break laws and pay the price.
Why this matters in practice
You will spend a lot of Haskell life inside monads. Database queries return IO (Either DbError a). HTTP handlers in servant run in Handler, which is ExceptT ServerError IO underneath. STM transactions run in STM. Parser combinators are monads. Test frameworks like hspec run in their own monad. The pattern of "do-notation, with <- for results, with the option to short-circuit or branch" recurs constantly.
The reason monads are worth learning rather than dodging: once you understand the abstraction, you can read code in any of these libraries the same way. The do-block in IO looks like the do-block in Parser looks like the do-block in STM. Different effects, same shape.
When Monad is the wrong tool
This is worth saying explicitly because the language nudges you toward Monad:
- If your computation does not need to inspect a result before choosing the next step,
Applicativeis more flexible (works withValidation, easier to analyze). - If you only need a pure transformation, you do not need any of these — just plain functions.
- If you need to combine multiple monadic effects (state + IO + errors), you are looking at monad transformers or an effect system, not a single monad. That is the subject of the next topic.
Common pitfalls
Treating do-notation as imperative code. It looks like Python, but the semantics are governed by the monad's >>=. In Maybe, a failed line skips the rest. In lists, a single line runs many times. The visual similarity to imperative code is a trap, not a feature.
Forgetting that pure / return is type-specific. pure 5 :: IO Int does no I/O. pure 5 :: [Int] is [5]. pure 5 :: Maybe Int is Just 5. Same expression, different meanings depending on the inferred type.
Using >>= when <$> would do. If you write m >>= \x -> pure (f x), that is just f <$> m. The compiler will not flag it; HLint will.
Stacking monads inside monads without transformers. IO (Maybe (Either Err a)) is technically fine but painful — every read of the inner value needs nested case or a tower of fmap. Monad transformers exist precisely for this.
Confusing Monad m => with "in the IO monad". A function with Monad m => works in any monad. A function with m ~ IO only works in IO. Beginners often write the second when they meant the first, then wonder why their code is hard to test.
Skipping the laws. Custom monads that break associativity will work in trivial examples and explode when refactored or used with monad transformers.
Key takeaways
- A Monad adds one capability over Applicative: the next step can depend on the result of the previous step.
>>=is the bind operator; do-notation is sugar that desugars to chains of>>=.- The behaviour of
>>=is per-type: short-circuit forMaybe, error-with-info forEither e, all-combinations for[], sequenced effects forIO. - Three laws — left identity, right identity, associativity — make refactoring do-blocks safe.
- Pick the weakest abstraction that works:
Functor<Applicative<Monad. Reaching forMonadwhenApplicativewould do narrows the types your function accepts. - Do-notation is not imperative code. It is a chain of binds rendered to look readable. Understand the desugaring and you understand the language.