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:
- How can we apply a function to several arguments, each supplied in a context?
- 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 bFor example:
fmap (+1) (Just (3 :: Int))
-- Just 4Here 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 5We 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 -- NothingMultiplication, 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 -> aConsequently, 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 bThe behavior on constructed Maybe values is:
Just g <*> Just a = Just (g a)
Nothing <*> _ = Nothing
_ <*> Nothing = NothingThese equations describe the existing instance; we do not redefine it. For example:
Just (+1) <*> Just 3
-- Just 4
(+) <$> Just 3 <*> Just 5
-- Just 8Recall 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 8If an argument is absent, the instance propagates that absence:
(+) <$> Just 3 <*> Nothing
-- NothingSupply 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 9We 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 bThe 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 3Its general qualified type is:
pure :: Applicative f => a -> f aYou 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 <*> valueFor example, these produce the same result:
fmap (+1) (Just 3) -- Just 4
pure (+1) <*> Just 3 -- Just 4We can therefore express the original addition in either form:
pure (+) <*> Just 3 <*> Just 5 -- Just 8
(+) <$> Just 3 <*> Just 5 -- Just 8The 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 -- MissingThe 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:
| Law | Equation |
|---|---|
| Identity | pure id <*> v = v |
| Homomorphism | pure g <*> pure a = pure (g a) |
| Interchange | u <*> pure a = pure (\g -> g a) <*> u |
| Composition | pure (.) <*> 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 4Interchange 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.25We 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 bRead it in three pieces:
| Piece | Meaning |
|---|---|
m a | The first contextual computation |
a -> m b | A function choosing a contextual computation from a supplied result |
m b | The combined computation |
For Maybe, bind behaves as follows:
Nothing >>= g = Nothing
Just a >>= g = g aIf 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
-- NothingThe first evaluation is:
safeReciprocal 4 >>= safeSqrt
= Just 0.25 >>= safeSqrt
= safeSqrt 0.25
= Just 0.5For 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
| Operation | Supplied function | Operation on the context |
|---|---|---|
fmap | a -> b | m a -> m b |
>>= | a -> m b | Combines 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.0Bind 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 bThe full class also supplies sequencing with >> and the historical return method. The superclass relationship is:
Functor
↑ required by
Applicative
↑ required by
MonadA 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 aPresent 3 >>= \a -> Present (a + 1)
-- Present 4
Missing >>= \a -> Present (a + 1)
-- MissingThe 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.0do 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) -- NothingWhat <- 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 doubledHere <- 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 4This 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 aBy 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 aA 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 xsThe 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 ajoin 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 -- NothingBoth 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 >>= idHere 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 aIntroducing a value and immediately passing it to a continuation agrees with calling the continuation directly.
Right identity
mx >>= pure = mxBinding 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
| Interface | Essential qualified type | What the caller supplies |
|---|---|---|
Functor | fmap :: Functor f => (a -> b) -> f a -> f b | An ordinary transformation and a contextual value |
Applicative | (<*>) :: Applicative f => f (a -> b) -> f a -> f b | A contextual function and a contextual argument |
Monad | (>>=) :: Monad m => m a -> (a -> m b) -> m b | A 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 8The 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 6Here 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 4Show solution
(*) <$> Just 3 <*> Just 4
-- Just 12Mapping partially applies multiplication to three. <*> then supplies four:
(*) <$> Just 3 <*> Just 4
= Just (3*) <*> Just 4
= Just (3 * 4)
= Just 122. Determine the result of a failed computation
(+) <$> Just 3 <*> NothingShow 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 4After 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 12Each <- 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 IntShow 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 -- NothingThis 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 <*> mymx and my are contextual values, while mf is a contextual function. Bind has the form:
mx >>= gHere 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
| ExpressionLayout 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.
| Concept | Qualified type or syntax | Purpose |
|---|---|---|
pure | Applicative f => a -> f a | Introduce a value into a context |
<*> | Applicative f => f (a -> b) -> f a -> f b | Apply a contextual function |
>>= | Monad m => m a -> (a -> m b) -> m b | Chain a result-dependent continuation |
join | Monad m => m (m a) -> m a | Combine nested instances of one context |
do | do ... | Express contextual sequencing with local names |
<- | name <- expression inside do | Bind 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 bPart 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 →