Part 9 of Haskell from First Principles. Continue from Part 8: Functors and the Laws of Mapping; its supplement on multiple type parameters explains partially applied constructors such as Either String.

Functor lets us apply an ordinary function to values in a computational context. Two questions take us further:

  1. How can we apply a function to several arguments, each supplied in a context?
  2. How can one contextual computation choose the next computation using its result?

The first motivates Applicative; the second motivates Monad. We will derive both through examples without assuming category theory.

Complete definitions belong in a .hs file; the expressions and commented results can be tried in GHCi after loading that file. Class definitions and specialized method signatures describe existing Prelude interfaces, so do not redeclare them. Evaluation steps and laws use text blocks because their = signs express mathematical equations rather than executable Haskell definitions. Treat alternative implementations as separate examples.

9.1 Recap: What can fmap do?

The familiar signature is:

fmap :: Functor f => (a -> b) -> f a -> f b

For example:

fmap (+1) (Just (3 :: Int))
-- Just 4

Here we specialize (+1) to Int -> Int, and fmap turns it into an operation of type Maybe Int -> Maybe Int. A Just value has its payload transformed; Nothing stays Nothing.

The function supplied to fmap has type a -> b. What happens when the function itself is supplied inside a Maybe, so its type is Maybe (a -> b)? fmap does not directly provide the required application operation.

9.2 The first problem: Add two Maybe values

Suppose we have these source definitions:

x :: Maybe Int
x = Just 3
 
y :: Maybe Int
y = Just 5

We want Just 8, with Nothing as the result when either argument is absent. Pattern matching handles this directly:

addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe Nothing _ = Nothing
addMaybe _ Nothing = Nothing
addMaybe (Just a) (Just b) = Just (a + b)
addMaybe (Just 3) (Just 5) -- Just 8
addMaybe (Just 3) Nothing  -- Nothing

Multiplication, subtraction, and comparison need the same handling of Nothing, with a different combining function. We want to express that common structure once.

Try fmap

Because functions are curried, (+) can accept one argument and return a function:

(+) :: Num a => a -> a -> a

Consequently, fmap (+) (Just (3 :: Int)) has type Maybe (Int -> Int). Conceptually, its value is Just (3+): a function that adds three, inside Maybe.

You cannot normally ask GHCi to display that value, because functions have no standard Show instance. You can inspect its type:

ghci> :type fmap (+) (Just (3 :: Int))
fmap (+) (Just (3 :: Int)) :: Maybe (Int -> Int)

We have reached the missing operation:

Maybe (Int -> Int)  +  Maybe Int  →  Maybe Int
   wrapped function    wrapped argument

<*> supplies that application.

9.3 The Applicative operator <*>

Compare the types of the two operations:

fmap  :: Functor f     =>   (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

<*> accepts the function inside the same kind of context as its argument. For Maybe, its specialized type is:

(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b

The behavior on constructed Maybe values is:

Just g <*> Just a = Just (g a)
Nothing <*> _     = Nothing
_ <*> Nothing     = Nothing

These equations describe the existing instance; we do not redefine it. For example:

Just (+1) <*> Just 3
-- Just 4
 
(+) <$> Just 3 <*> Just 5
-- Just 8

Recall that <$> is infix fmap. Both <$> and <*> associate to the left at the same precedence, so the second expression groups as ((+) <$> Just 3) <*> Just 5:

(+) <$> Just 3 <*> Just 5
  = Just (3+) <*> Just 5
  = Just (3 + 5)
  = Just 8

If an argument is absent, the instance propagates that absence:

(+) <$> Just 3 <*> Nothing
-- Nothing

Supply more arguments

Each application supplies another argument to a curried function:

sum3 :: Int -> Int -> Int -> Int
sum3 a b c = a + b + c
 
-- sum3 <$> Just 2 <*> Just 3 <*> Just 4
-- Just 9

We choose the ordinary combining function; the instance handles its contextual arguments. The arguments are specified without obtaining one argument’s result to choose the next computation. This independence is about value dependencies, not execution in parallel. An Applicative instance can impose sequencing, and Maybe can short-circuit when an input is absent.

9.4 The Applicative class and pure

The essential interface is:

-- Simplified presentation of the existing Prelude class.
class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

The superclass constraint requires every Applicative to have a Functor instance. The full class also provides liftA2, *>, and <*; defining pure and <*> suffices for our example instance.

pure introduces a value into the context appropriate to the instance:

pure 3 :: Maybe Int
-- Just 3
 
pure 3 :: [Int]
-- [3]
 
pure 3 :: Either String Int
-- Right 3

Its general qualified type is:

pure :: Applicative f => a -> f a

You can think of this as introducing a value without adding extra contextual behavior. Its precise implementation depends on the instance; it need not allocate a container or perform an external action.

Connect pure, <*>, and fmap

The Functor and Applicative operations must agree:

fmap g value = pure g <*> value

For example, these produce the same result:

fmap (+1) (Just 3)    -- Just 4
pure (+1) <*> Just 3  -- Just 4

We can therefore express the original addition in either form:

pure (+) <*> Just 3 <*> Just 5 -- Just 8
(+) <$> Just 3 <*> Just 5      -- Just 8

The latter starts directly with fmap and is often more convenient.

9.5 Implement an Applicative instance

The custom Optional type from Part 8 keeps the mechanics visible. This is a complete source example:

data Optional a
  = Missing
  | Present a
  deriving (Eq, Show)
 
instance Functor Optional where
  fmap _ Missing = Missing
  fmap g (Present a) = Present (g a)
 
instance Applicative Optional where
  pure a = Present a
 
  Missing <*> _ = Missing
  _ <*> Missing = Missing
  Present g <*> Present a = Present (g a)

pure uses Present. Application propagates Missing; when both operands are Present, it applies the contained function to the contained argument.

(+) <$> Present 3 <*> Present 5 -- Present 8
(+) <$> Present 3 <*> Missing   -- Missing

The instance centralizes the contextual behavior. A caller can then combine optional arguments with any suitably typed function.

Applicative for lists

With lists, the context represents multiple possible values:

(+) <$> [1,2] <*> [10,20]
-- [11,21,12,22]

The standard list instance applies every function to every argument, in this order:

fs <*> xs = [g a | g <- fs, a <- xs]

First, (+) <$> [1,2] produces the conceptual function list [(1+), (2+)]. Applying it to [10,20] produces the two results from (1+), then the two results from (2+).

This is the ordinary list instance’s Cartesian-product behavior. Applicative is a general interface; the particular instance decides what combining contexts means.

9.6 Applicative laws

A lawful instance obeys four equations:

LawEquation
Identitypure id <*> v = v
Homomorphismpure g <*> pure a = pure (g a)
Interchangeu <*> pure a = pure (\g -> g a) <*> u
Compositionpure (.) <*> u <*> v <*> w = u <*> (v <*> w)

Identity says that contextual application of identity changes nothing. Homomorphism says that introducing an ordinary function and argument with pure, then applying them, agrees with applying first and introducing the result:

pure (+1) <*> pure 3 :: Maybe Int -- Just 4
pure ((+1) 3) :: Maybe Int        -- Just 4

Interchange ensures consistency when the argument is supplied with pure. Composition ensures that contextual application respects ordinary function composition, including how the contexts are combined.

These are equations of behavior at suitable types, not Haskell (==) tests on arbitrary functions. The compiler checks types but does not prove custom instances lawful. The standard Applicative documentation specifies these laws and the agreement with fmap.

9.7 The second problem: Dependent computations

Consider two operations: take a reciprocal, then take its square root. Each operation may reject an input:

safeReciprocal :: Double -> Maybe Double
safeReciprocal a
  | a == 0    = Nothing
  | otherwise = Just (1 / a)
 
safeSqrt :: Double -> Maybe Double
safeSqrt a
  | a < 0     = Nothing
  | otherwise = Just (sqrt a)

These teaching functions check division by zero and negative square roots. They are not complete floating-point validation: Double also has infinities and NaN, and a reciprocal can overflow or underflow. The examples below use small finite inputs.

safeReciprocal 4
-- Just 0.25

We want to give 0.25 to safeSqrt and obtain Just 0.5. But safeReciprocal returns Maybe Double, whereas safeSqrt accepts an ordinary Double.

Why fmap leaves another layer

Mapping the second operation gives:

fmap safeSqrt (safeReciprocal 4)
-- Just (Just 0.5)

The result has type Maybe (Maybe Double). The outer Maybe comes from mapping over the first result; the inner Maybe comes from safeSqrt itself.

In general, we want to compose functions shaped like and . Ordinary composition cannot directly connect them because the first produces while the second accepts . We need a way to connect the computations and combine their contextual behavior. That is the role of monadic bind.

9.8 Bind: The operator >>=

The signature is:

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

Read it in three pieces:

PieceMeaning
m aThe first contextual computation
a -> m bA function choosing a contextual computation from a supplied result
m bThe combined computation

For Maybe, bind behaves as follows:

Nothing >>= g = Nothing
Just a >>= g  = g a

If the first result is absent, the continuation is skipped. If it is present, the continuation receives its payload. Consequently:

safeReciprocal 4 >>= safeSqrt
-- Just 0.5
 
safeReciprocal 0 >>= safeSqrt
-- Nothing
 
safeReciprocal (-4) >>= safeSqrt
-- Nothing

The first evaluation is:

safeReciprocal 4 >>= safeSqrt
  = Just 0.25 >>= safeSqrt
  = safeSqrt 0.25
  = Just 0.5

For zero, the reciprocal returns Nothing, so safeSqrt is skipped. For negative four, the reciprocal produces Just (-0.25), and safeSqrt rejects that negative argument.

Compare mapping and binding

OperationSupplied functionOperation on the context
fmapa -> bm a -> m b
>>=a -> m bCombines m a with that continuation to produce m b

The variable b in fmap can itself be a contextual type. Mapping does not flatten it:

fmap safeSqrt (Just 4) -- Just (Just 2.0)
Just 4 >>= safeSqrt   -- Just 2.0

Bind connects a continuation that already produces a context, combining the two layers into one result.

9.9 The Monad type class

The essential interface is:

-- Simplified presentation of the existing Prelude class.
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b

The full class also supplies sequencing with >> and the historical return method. The superclass relationship is:

Functor
   ↑ required by
Applicative
   ↑ required by
Monad

A type constructor with a Monad instance must therefore also have Applicative and Functor instances, with compatible operations.

A Monad instance for Optional

Add this instance to the complete Optional example in Section 9.5:

instance Monad Optional where
  Missing >>= _ = Missing
  Present a >>= g = g a
Present 3 >>= \a -> Present (a + 1)
-- Present 4
 
Missing >>= \a -> Present (a + 1)
-- Missing

The implementation delegates the success case directly to the continuation. It must not add an extra Present, because g a already has type Optional b.

9.10 do notation

Several binds with nested lambdas can be difficult to read:

safeReciprocal 4 >>= \r ->
  safeSqrt r >>= \s ->
    pure (s * 10)
-- Just 5.0

do notation expresses the same structure with local names:

calculate :: Double -> Maybe Double
calculate a = do
  r <- safeReciprocal a
  s <- safeSqrt r
  pure (s * 10)
calculate 4    -- Just 5.0
calculate 0    -- Nothing
calculate (-4) -- Nothing

What <- means

In r <- safeReciprocal a, the name r is available to the subsequent statements. The binding happens through the instance’s sequencing behavior. For Maybe, a Just result provides its payload, while Nothing makes the combined result Nothing without calling the continuation.

This is a local immutable binding rather than mutable assignment. Its general meaning depends on the instance: lists can supply several possible values to a continuation rather than a single successful value.

Translate ordinary do into bind

Consider this schematic expression, with suitable contextual values mx and my:

do
  a <- mx
  b <- my
  pure (a + b)

With ordinary monadic do notation, it translates to:

mx >>= \a ->
  my >>= \b ->
    pure (a + b)

The rest of the block sits inside the continuation for mx. A later computation can therefore use a, as safeSqrt r uses r in calculate. In the schematic block above, a preexisting value my has no dependency on the newly bound a.

These examples use ordinary do, without GHC’s ApplicativeDo extension, which can translate suitable independent statements using Applicative operations. Statements without <- use sequencing such as >>. A refutable pattern, such as Just a <- action, also needs a failure path through MonadFail; the simple variable patterns above cannot fail to match. The GHC guide describes do notation and pattern failure.

let inside do

An ordinary local definition uses let, without in:

calculateDoubled :: Double -> Maybe Double
calculateDoubled a = do
  r <- safeReciprocal a
  let doubled = r * 2
  safeSqrt doubled

Here <- obtains the value according to the contextual computation’s bind behavior. let defines an ordinary pure value. The final expression is already Maybe Double, so it does not need another pure.

return does not exit a function

Older examples often write return (a + b) where newer examples write pure (a + b). For lawful Monad instances, return agrees with pure; modern instances inherit its default implementation from pure.

Neither means an early exit from a function. They introduce a value into the context:

Just 3 >>= \a -> pure (a + 1)
-- Just 4

This article uses pure, which also works when only an Applicative constraint is needed.

9.11 Use Either to preserve an error message

Maybe indicates presence or absence without an explanation. The standard Either type provides a place for an error:

-- Already provided by the Prelude:
-- data Either e a = Left e | Right a

By convention, Left carries an error and Right carries success. Define:

reciprocal :: Double -> Either String Double
reciprocal a
  | a == 0    = Left "Division by zero"
  | otherwise = Right (1 / a)
 
sqrtChecked :: Double -> Either String Double
sqrtChecked a
  | a < 0     = Left "Negative square root"
  | otherwise = Right (sqrt a)
 
calculateChecked :: Double -> Either String Double
calculateChecked a = do
  r <- reciprocal a
  s <- sqrtChecked r
  pure (s * 10)

These have the same floating-point limits as the Maybe teaching examples. For the inputs considered here:

calculateChecked 4
-- Right 5.0
 
calculateChecked 0
-- Left "Division by zero"
 
calculateChecked (-4)
-- Left "Negative square root"

The standard Monad (Either e) instance fixes the error type e and chains the success values:

Left err >>= _ = Left err
Right a >>= g  = g a

A Left skips the continuation and preserves the error. This is useful for parsing and other calculations that can fail at intermediate steps. This particular sequencing behavior reports the encountered error; it does not accumulate all possible errors from independent checks.

The structure of the code is almost identical to the Maybe version. Changing the instance changes the meaning of the surrounding context.

9.12 Monads also describe multiple results

Lists have a Monad instance too. Its bind behavior is:

xs >>= g = concatMap g xs

The standard function has type:

concatMap :: (a -> [b]) -> [a] -> [b]

It applies the function to each element and concatenates the resulting lists:

[1,2,3] >>= \a -> [a, -a]
-- [1,-1,2,-2,3,-3]

With do notation:

pairs :: [(Int, Int)]
pairs = do
  a <- [1,2]
  b <- [10,20]
  pure (a,b)
 
-- pairs
-- [(1,10),(1,20),(2,10),(2,20)]

The first binding supplies each possibility for a; for each one, the second supplies each possibility for b. This agrees with the list comprehension:

[(a,b) | a <- [1,2], b <- [10,20]]
-- [(1,10),(1,20),(2,10),(2,20)]

Monad instances are therefore useful even in pure code without external effects. Lists combine possible results, Maybe propagates absence, and Either propagates errors. IO uses the same interface to compose input/output actions, which Part 10 explores.

9.13 The connection to join

Mapping safeSqrt over a Maybe Double produced nested optionality. An operation that combines the two layers would solve that problem:

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

join is available from Control.Monad. Add the import at the top of a source file, before any definitions:

import Control.Monad (join)

Then try:

join (Just (Just (3 :: Int))) -- Just 3
join (Just Nothing) :: Maybe Int -- Nothing
join Nothing :: Maybe Int        -- Nothing

Both layers must use the same constructor m. join is not a general operation for unwrapping arbitrary nested types.

Derive bind from mapping and flattening

Given g :: a -> m b and mx :: m a, mapping produces fmap g mx :: m (m b). Applying join then produces m b. The connection is:

mx >>= g = join (fmap g mx)

Conversely:

join nested = nested >>= id

Here id receives a value already of type m a and returns it unchanged as the continuation’s contextual result. These equations explain the relationship rather than introducing new definitions of the standard operators.

For lists, join is concatenation of a list of lists:

join [[1,2], [3], []]
-- [1,2,3]

For Maybe, it combines absence or presence across the two layers. For Either e, it preserves an error from either layer or produces the inner success value:

join (Right (Left "inner error")) :: Either String Int
-- Left "inner error"

A monad thus combines mapping with a coherent way to flatten nested instances of its context.

9.14 The three Monad laws

For suitable values and functions g :: a -> m b and h :: b -> m c, lawful instances obey:

Left identity

pure a >>= g = g a

Introducing a value and immediately passing it to a continuation agrees with calling the continuation directly.

Right identity

mx >>= pure = mx

Binding a computation to pure preserves its meaning.

Associativity

(mx >>= g) >>= h = mx >>= (\a -> g a >>= h)

Regrouping the same dependent computations preserves their meaning. This does not permit arbitrarily reordering them: a later computation can depend on an earlier result, and contextual sequencing can matter.

The laws support algebraic reasoning about code. A Monad instance’s inherited operations must also agree with bind; for example:

fmap g mx = mx >>= (pure . g)
mf <*> mx = mf >>= (\g -> mx >>= (\a -> pure (g a)))

These compatibility requirements keep the Functor, Applicative, and Monad views of the same constructor coherent. The standard Monad documentation gives the class contract.

9.15 Compare Functor, Applicative, and Monad

InterfaceEssential qualified typeWhat the caller supplies
Functorfmap :: Functor f => (a -> b) -> f a -> f bAn ordinary transformation and a contextual value
Applicative(<*>) :: Applicative f => f (a -> b) -> f a -> f bA contextual function and a contextual argument
Monad(>>=) :: Monad m => m a -> (a -> m b) -> m bA contextual value and a continuation choosing the next computation

With Applicative, the contextual inputs can be specified without inspecting each other’s results:

(+) <$> Just 3 <*> Just 5
-- Just 8

The combining function still uses both values. What is absent is a result-dependent choice of the next contextual input.

Bind allows that choice:

Just 3 >>= \a ->
  if a > 0
  then Just (a * 2)
  else Nothing
-- Just 6

Here the obtained a selects the next Maybe computation. In the reciprocal example, it supplies the argument to a subsequent operation that can itself fail.

Use the interface that expresses the dependency you need. Applicative is often enough for a fixed combination of inputs; Monad adds the ability for one result to determine the next computation.

9.16 Exercises

Attempt each exercise before opening its solution.

1. Evaluate an Applicative expression

(*) <$> Just 3 <*> Just 4
Show solution
(*) <$> Just 3 <*> Just 4
-- Just 12

Mapping partially applies multiplication to three. <*> then supplies four:

(*) <$> Just 3 <*> Just 4
  = Just (3*) <*> Just 4
  = Just (3 * 4)
  = Just 12

2. Determine the result of a failed computation

(+) <$> Just 3 <*> Nothing
Show solution

The result is Nothing. The first step produces the conceptual value Just (3+), but the second argument is absent. The Maybe Applicative instance propagates that absence.

3. Compare mapping and binding

What is the difference between these expressions?

fmap (\a -> Just (a + 1)) (Just 3)
Just 3 >>= (\a -> Just (a + 1))
Show solution
fmap (\a -> Just (a + 1)) (Just 3)
-- Just (Just 4)
 
Just 3 >>= (\a -> Just (a + 1))
-- Just 4

After specializing the numbers to Int, their types are Maybe (Maybe Int) and Maybe Int, respectively. fmap preserves the outer Just and places the function’s entire result inside it. Bind combines the existing context with the context returned by the continuation.

4. Rewrite with do notation

Just 3 >>= \a ->
  Just 4 >>= \b ->
    pure (a * b)
Show solution
do
  a <- Just 3
  b <- Just 4
  pure (a * b)
-- Just 12

Each <- names a value for the remainder of the block. The final pure introduces the product into Maybe.

5. Evaluate list bind

[1,2] >>= \a -> [a, a + 10]
Show solution
[1,2] >>= \a -> [a, a + 10]
-- [1,11,2,12]

The continuation produces [1,11] for the first element and [2,12] for the second. List bind concatenates them in that order.

6. Add optional integers with Monad

Write an implementation using bind or do, rather than Applicative application:

addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
Show solution

Use this as an alternative to the pattern-matching definition in Section 9.2:

addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe mx my = do
  a <- mx
  b <- my
  pure (a + b)

Equivalently, its body is mx >>= \a -> my >>= \b -> pure (a + b). If either input is Nothing, bind propagates it; otherwise the result is the sum inside Just.

addMaybe (Just 3) (Just 5) -- Just 8
addMaybe Nothing (Just 5)  -- Nothing
addMaybe (Just 3) Nothing  -- Nothing

This matches the earlier Applicative implementation, (+) <$> mx <*> my. There is no result-dependent choice of my, so Monad is available but not required for this particular computation. Here “safe” means handling absent inputs; bounded Int arithmetic can still overflow.

9.17 Haskell grammar recap

The main Applicative expression forms are:

pure a
g <$> mx
mf <*> mx
combine <$> mx <*> my

mx and my are contextual values, while mf is a contextual function. Bind has the form:

mx >>= g

Here g accepts an ordinary result and returns another contextual computation.

A simplified grammar for ordinary do notation is:

DoExpression := do { Statement; ...; FinalExpression }
 
Statement := Pattern <- Expression
           | let Declarations
           | Expression

Layout normally supplies the braces and semicolons. A block can contain binding statements, ordinary let declarations, and contextual expressions whose results are discarded. Its final statement is an expression of the appropriate contextual type, rather than a binding statement.

do
  a <- Just 3
  let b = a + 1
  pure (b * 2)
-- Just 8

<- obtains values using the contextual sequencing behavior; let introduces ordinary local definitions. Variable patterns are sufficient for the examples in this lesson. More general refutable patterns add the MonadFail requirement explained in Section 9.10.

ConceptQualified type or syntaxPurpose
pureApplicative f => a -> f aIntroduce a value into a context
<*>Applicative f => f (a -> b) -> f a -> f bApply a contextual function
>>=Monad m => m a -> (a -> m b) -> m bChain a result-dependent continuation
joinMonad m => m (m a) -> m aCombine nested instances of one context
dodo ...Express contextual sequencing with local names
<-name <- expression inside doBind a value supplied through sequencing

The class requirements build from Functor to Applicative to Monad. Mapping supplies transformation; Applicative combines contextual inputs; bind lets an obtained result determine another contextual computation. The central type to recognize is:

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

Part 10: IO and Real Haskell Programs puts these sequencing ideas to work with main :: IO (), putStrLn :: String -> IO (), and getLine :: IO String, while preserving Haskell’s pure treatment of action values.

Adapted from the GPT-generated Block 9 tutorial supplied for this series, with worked exercise solutions and technical clarifications.

← Previous: Functors and the laws of mapping · Part 8 supplement · Series index · Next: IO and real Haskell programs →