Skip to content

Module 07.2: Monads — Composing Computations

In Module 07.1, we discovered that fmap generalizes the idea of map: given a function a -> b, we can transform values inside a list, tree, Maybe, or another functor.

Now consider functions that return a Maybe themselves. How do we compose those functions without repeatedly writing code to check for Nothing?

That small problem will lead us to a surprisingly general abstraction: monads.

By the end of this module, you should be able to:

  • explain why ordinary composition fails for Maybe-producing functions,
  • derive and use (>>=) (pronounced bind),
  • understand pure,
  • read a small Monad instance,
  • use do notation, and
  • describe how the same interface supports different kinds of computations.

How to use this module

When you see a box labeled Gradescope question, answer that question in the Module 7.2 Completion assignment on Gradescope.


Ordinary Function Composition

Recall function composition (.) from Module 02.2. Take two ordinary functions:

double :: Double -> Double
double x = 2 * x

increment :: Double -> Double
increment x = x + 1

We can compose them:

(increment . double) 5  -- 11.0

Composition works because the first function returns the kind of value the second function expects:

f :: a -> b
g :: b -> c

-- g . f :: a -> c

But some useful functions don't return an ordinary value directly.


A Square Root That Might Fail

Recall that Maybe distinguishes a present value from a missing one. Let's make a function that declines to compute real square roots of negative numbers:

safeSqrt :: Double -> Maybe Double
safeSqrt x =
  if x < 0
    then Nothing
    else Just (sqrt x)

Examples:

safeSqrt 9      -- Just 3.0
safeSqrt (-4)   -- Nothing

Now suppose we want a safe fourth root. Mathematically, we can take the square root twice. It is tempting to write:

safeFourthRoot = safeSqrt . safeSqrt  -- TYPE ERROR

The types don't fit:

safeSqrt :: Double -> Maybe Double

first result:       Maybe Double
second input needs: Double

The second safeSqrt expects a Double, not a Maybe Double.

Gradescope question: Why doesn't composition work?

Submit this response on Gradescope.

Explain why safeSqrt . safeSqrt is ill-typed. Be specific about the output type of the first call and the input type of the next call.


A Working Solution, with Repeated Bookkeeping

We already know how to solve the problem with pattern matching:

safeFourthRoot :: Double -> Maybe Double
safeFourthRoot x =
  case safeSqrt x of
    Nothing -> Nothing
    Just y  -> safeSqrt y

This handles both possibilities. If the first square root fails, the whole calculation fails. Otherwise, we feed the value to the next square root.

What about an eighth root?

safeEighthRoot :: Double -> Maybe Double
safeEighthRoot x =
  case safeFourthRoot x of
    Nothing -> Nothing
    Just y  -> safeSqrt y

We're repeating a pattern:

case someResult of
  Nothing -> Nothing
  Just x  -> nextStep x

Most of this code has nothing to do with square roots. It is the same failure-handling plumbing every time. That suggests a familiar course theme: factor out what repeats.


Extract a Reusable Operation

Let's give that pattern a name:

andThen :: Maybe a -> (a -> Maybe b) -> Maybe b
andThen Nothing  _ = Nothing
andThen (Just x) f = f x

Read the type slowly:

Maybe a          ->   (a -> Maybe b)    -> Maybe b
previous result       next computation    final result

If the previous result is Nothing, we stop. If it is Just x, we give x to the next function. Notice that this next function can also fail.

Now the earlier definitions become:

safeFourthRoot x = safeSqrt x `andThen` safeSqrt

safeEighthRoot x =
  safeSqrt x
    `andThen` safeSqrt
    `andThen` safeSqrt

The checks are gone from the main program: they live in andThen.

This operation is so useful that Haskell has a standard name for it:

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

It is called bind. Maybe already implements it, with the behavior we just defined as andThen.

safeFourthRoot x = safeSqrt x >>= safeSqrt

The key idea is not the strange-looking operator. It is the repeated structure that the operator eliminates.

Gradescope question: Follow bind

Submit this response on Gradescope.

Predict the results and briefly explain how (>>=) behaves in each case:

Just 16 >>= safeSqrt
Nothing >>= safeSqrt
safeSqrt 9 >>= safeSqrt

Composing Different Possibly Failing Operations

This is not just a special trick for repeated square roots. Consider another operation that might fail:

safeReciprocal :: Double -> Maybe Double
safeReciprocal 0 = Nothing
safeReciprocal x = Just (1 / x)

We can combine it with square root:

safeSqrt 4 >>= safeReciprocal
-- Just 0.5

safeSqrt (-4) >>= safeReciprocal
-- Nothing

The two functions have different jobs, but the same result structure. Bind takes care of propagating failure consistently.

Notice the distinction from Module 07.1:

fmap (+1) (Just 5)
-- Just 6

Just 5 >>= safeReciprocal
-- Just 0.2

fmap expects a function of type a -> b, returning an ordinary value.

>>= expects a function of type a -> m b, returning another value wrapped in a context.

Here context just means the wrapper around a value. For Maybe, the context is whether a value is present at all (Just) or missing (Nothing). The rest of this module uses the word in that sense.

If we mistakenly tried:

fmap safeSqrt (Just 16)

we would get:

Just (Just 4.0)

A nested Maybe! Bind knows how to sequence the computation without accumulating an extra layer of Maybe.

Gradescope question: fmap versus bind

Submit this response on Gradescope.

Compare the types of fmap and (>>=). Why does fmap safeSqrt (Just 16) produce a different shape of result from Just 16 >>= safeSqrt?


Putting an Ordinary Value into a Context: pure

We also need a simple way to produce an ordinary successful result inside a context:

pure 5 :: Maybe Int
-- Just 5

The general type is:

pure :: Applicative m => a -> m a

(Applicative is another typeclass; the next section says just enough about it.)

For Maybe, pure behaves like Just. It does not check that a calculation is safe. It only places an already-computed value into the chosen context.

So the two play different roles: a safe operation such as safeReciprocal can introduce a failure; pure never does. It always succeeds.

Older Haskell materials commonly use return for this operation. Despite the name, Haskell's return is not the return statement from Python or Java: it doesn't exit a function, it just wraps a value, exactly like pure. In modern Haskell, we will use pure.


The Absolute Minimum About Applicatives

We have seen Functor, which provides fmap. Modern Haskell places another typeclass between Functor and Monad:

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

The idea of <*> is to apply a function inside a context to an argument inside a context:

Just (+1) <*> Just 5  -- Just 6
Just (+1) <*> Nothing -- Nothing

For our purposes, the takeaway is small: Applicative is where pure is defined, and (as the next section shows) every Monad is also an Applicative. We won't go further into applicatives in this course.

Gradescope question: What does pure do?

Submit this response on Gradescope.

What does pure 5 :: Maybe Int produce? Why should we not use pure (1 `div` 0) :: Maybe Int as a replacement for a safe division function?


The Monad Typeclass and a Concrete Instance

Here's the relevant portion of the modern class definition:

class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b

The relationship is:

Functor        Applicative          Monad
  fmap    --->   pure, <*>    --->     >>=

The Applicative m => in the class header is a superclass constraint, like Eq a => Ord a in Module 07.1. Every Monad must also be an Applicative, and (from Functor f => Applicative f in the previous section) every Applicative must also be a Functor.

To see that these are just ordinary Haskell functions, look at illustrative definitions for Maybe. They already exist in the standard library; don't paste these instance declarations into your code.

instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap f (Just x) = Just (f x)

instance Applicative Maybe where
  pure = Just
  Nothing <*> _ = Nothing
  Just f  <*> x = fmap f x

instance Monad Maybe where
  Nothing >>= _ = Nothing
  Just x  >>= f = f x

The Monad part is exactly our earlier andThen pattern. Nothing supernatural happened: we found a useful repetitive operation and gave it a shared interface.


do Notation: Easier to Read, Same Meaning

Long chains of binds can become hard to read:

example :: Double -> Maybe Double
example x =
  safeSqrt x >>= \a ->
  safeReciprocal a >>= \b ->
  pure (a + b)

Haskell's do notation is a more readable way to write the same sequence:

example :: Double -> Maybe Double
example x = do
  a <- safeSqrt x
  b <- safeReciprocal a
  pure (a + b)

The translation to remember is:

do
  a <- first
  b <- second a
  finish a b

means (roughly):

first >>= \a ->
second a >>= \b ->
finish a b

For Maybe, the rest of the block is skipped if any preceding step produces Nothing. The do block itself does not perform the check; the Maybe instance of bind does.

Four practical rules

1. Use <- for computations in the context. If safeSqrt x :: Maybe Double, then:

a <- safeSqrt x

binds a :: Double within the remainder of the do block when the computation succeeds. This is not mutable assignment.

2. Use let for ordinary values. For example:

let doubled = 2 * a

There is no in in this do-block let statement. This would not be appropriate:

doubled <- 2 * a   -- WRONG: right side is not a Maybe computation

3. The final line determines the result of the do block. For instance:

pure doubled

produces a Maybe Double. A final line that is just doubled would have type Double, not Maybe Double.

4. A computation on a line by itself runs, but its result is discarded. A line with no <- and no let still takes part in the sequence, so in Maybe it can still make the whole block fail. You write a line like this when you care about what the step does (here, possibly failing) and not about the value it produces.

One complete example

Here is a single do block that uses all four rules. First, one more small helper. It either succeeds with no interesting value or fails:

atMost :: Double -> Double -> Maybe ()
atMost limit y = if y <= limit then Just () else Nothing

Its result type is Maybe (). Just () means "the check passed," and Nothing means "the check failed." The () inside carries no information.

Now the example:

example2 :: Double -> Maybe Double
example2 x = do
  a <- safeSqrt x          -- (1) bind: a :: Double; this step could fail
  let b = a - 3            -- (2) let: an ordinary value; this step cannot fail
  atMost 100 b             -- (3) bare: may fail; its () result is thrown away
  c <- safeReciprocal b    -- (4) bind again, using b
  pure (a + c)             -- (5) final line: the result of the whole block

Look at each line:

  • Line 1 uses <- because safeSqrt x is a Maybe Double, and we want the Double inside.
  • Line 2 uses let because a - 3 is a plain Double. There is nothing to unwrap and nothing that can fail.
  • Line 3 is a bare computation of type Maybe (). We could write _ <- atMost 100 b, but naming a () is pointless. We can't leave the line out, though: if the check fails, the whole block is Nothing. The value gets discarded, but the success or failure does not.
  • Line 4 uses <- again, and it can use b from line 2.
  • Line 5 is pure (a + c), so the block has type Maybe Double, which matches the signature.

Tracing a few inputs shows that each Maybe line can be the one that stops the block:

x a (line 1) b (line 2) atMost 100 b (line 3) c (line 4) result
16 4.0 1.0 Just () 1.0 Just 5.0
-4 — — — — Nothing, stopped at line 1
9 3.0 0.0 Just () — Nothing, stopped at line 4
1000000 1000.0 997.0 Nothing — Nothing, stopped at line 3

Here is the same function written out with >>=. Haskell turns the do block into exactly this:

example2 x =
  safeSqrt x >>= \a ->
  let b = a - 3 in
  atMost 100 b >>= \_ ->
  safeReciprocal b >>= \c ->
  pure (a + c)

Each kind of line has a direct translation:

In a do block Means
x <- m m >>= \x -> ...rest...
let x = e let x = e in ...rest...
m (bare) m >>= \_ -> ...rest... (Haskell also spells this m >> ...rest...)
final line stays as-is, and must have the monadic type (here, Maybe Double)

The bare line becomes a bind whose lambda ignores its argument (\_ ->). That is why its result is discarded but its failure still counts: Nothing >>= \_ -> ... is Nothing, like any other bind.

You will see bare lines most often with IO, in the next section. A line like putStrLn "Hello" in a do block is a bare computation for the same reason: we want it to happen, and its () result is useless.

Try it in ghci - nothing to submit

Put safeSqrt, safeReciprocal, atMost, and example2 in a file, load it in ghci, and run example2 on the four inputs from the table. Then delete line 3 and reload. Which input's result changes, and why?

Gradescope question: Read and rewrite do

Submit this response on Gradescope.

Rewrite this definition using do notation:

f x = safeSqrt x >>= \a ->
      safeReciprocal a >>= \b ->
      pure (a + b)

If safeSqrt x produces Nothing, does safeReciprocal run? Explain which operation is responsible for the behavior.


Other Meanings of Sequencing

Maybe is a monad whose sequencing means continue on success, stop on failure. But the same interface works elsewhere.

Lists: A computation can have multiple results. Bind applies the function to each value and concatenates the resulting lists:

[1, 2] >>= \x -> [x, x * 10]
-- [1,10,2,20]

IO: A computation can perform input/output operations, which we can sequence with do:

main :: IO ()
main = do
  name <- getLine
  putStrLn ("Hello, " ++ name)

Here getLine :: IO String is a computation that reads a line of input and produces a String, and putStrLn :: String -> IO () prints a line. (() is Haskell's "no interesting result" type.) This is how a standalone Haskell program does input and output. So far in this course you've only run code through ghci and test suites, so you haven't needed it.

Parsers: The course's Parser type from Module 06.2 is also a monad (see the ParserBase Reference). But be careful with a look-alike operator: the >>=: you used in 06.2 is not bind. Specialized to parsers, it behaves like this:

(>>=:) :: Parser a -> (a -> b) -> Parser b
p >>=: f = fmap f p

It transforms a parser's result with an ordinary function a -> b, so it's really fmap from Module 07.1 with its arguments flipped. Real bind, >>=, takes a function a -> Parser b, which would let the next parser depend on what was just parsed. We didn't need that in 06.2.

Different monads give different meanings to sequencing. do notation does not itself skip steps, produce multiple results, or perform I/O; the type and its >>= determine the behavior.


Three Short Laws

Just as functors obey laws, monads obey laws describing sensible sequencing. Here m means an existing monadic computation, and f and g are functions that produce monadic computations.

Left identity:

pure x >>= f   =   f x

Wrapping an ordinary value before immediately using it changes nothing.

Right identity:

m >>= pure   =   m

Taking a result and wrapping it straight back up changes nothing.

Associativity:

(m >>= f) >>= g   =   m >>= (\x -> f x >>= g)

Grouping a sequence differently doesn't change its meaning. Importantly, this does not mean that we can reorder the steps.

These are laws of behavioral equivalence, not necessarily executable Haskell == tests; not every monadic type supports Eq.


Where Homework 7 Goes Next

We've learned to remove repetitive failure checks from programs that use Maybe. But failure isn't the only kind of bookkeeping.

Suppose you have a tiny arithmetic-language evaluator:

data Term = Num Int | Div Term Term

eval :: Term -> Int

What if you later want it to report errors, count evaluation steps, or produce a trace? A naive solution can bury the actual evaluation algorithm under error checks, a step counter passed through every recursive call, or string concatenation for the trace.

In Homework 7, you'll build these variations and discover how the same sequencing abstraction can keep the core evaluator understandable.

The recurring idea of this course is once again abstraction through factoring out repetition. Monads provide a standard interface for abstracting the plumbing around a sequence of computations.


Finish Module 07.2

Make sure you have answered the five Gradescope questions embedded above:

  1. Why is safeSqrt . safeSqrt ill-typed?
  2. How does (>>=) behave on Just and Nothing?
  3. Why do fmap safeSqrt and >>= safeSqrt produce different shapes of result?
  4. What does pure do, and why isn't it a safety check?
  5. How do you rewrite a chain of binds using do notation?

Then answer the final two questions.

Gradescope question: Time spent

Submit this response on Gradescope.

Approximately how much time did you spend on this module?

  • less than 30 minutes
  • 30 - 45 minutes
  • 45 - 60 minutes
  • 60 - 75 minutes
  • 75 - 90 minutes
  • more than 90 minutes

Gradescope question: Lingering questions

Submit this response on Gradescope.

What lingering questions or thoughts do you have about this module?