Monads Without Mysticism
A practical visual introduction to >>=, do-notation and sequencing computations whose next step depends on the previous result.
Monads have accumulated an impressive amount of mythology.
They have been called burritos, spacesuits, programmable semicolons, monoids in categories of endofunctors, and occasionally things one must understand only after “the epiphany.”
Let us ignore all of that for a while.
We already know almost everything we need.
A Functor gave us:
fmap :: (a -> b) -> m a -> m b
An Applicative let contextual functions consume contextual values:
(<*>) :: m (a -> b) -> m a -> m b
A Monad answers one additional question:
What if the function we want to run next must look at the previous value — and itself returns another contextual value?
Meet bind
The central operation is:
(>>=) :: m a -> (a -> m b) -> m b
Read it from left to right:
m a
then a function that needs the inner a
and may produce another m b
finally yielding m b
The operator is called bind.
ma >>= f takes a contextual value ma. If the context permits a value to continue, bind supplies that value to f. The function f may then decide what contextual computation comes next.
A concrete Maybe example
safeHalf :: Int -> Maybe Int
safeHalf n
| even n = Just (n `div` 2)
| otherwise = Nothing
safeRoot :: Double -> Maybe Double
safeRoot x
| x >= 0 = Just (sqrt x)
| otherwise = Nothing
Now we can chain computations that can fail:
Just 20 >>= safeHalf
-- Just 10
and continue:
Just 20
>>= safeHalf
>>= (safeRoot . fromIntegral)
Why fmap is not enough
Suppose we tried:
fmap safeHalf (Just 20)
The result is:
Just (Just 10)
because safeHalf already returns Maybe Int.
We have accidentally created two layers of context:
Maybe (Maybe Int)
Bind both maps the function and flattens the resulting nested context:
Just 20 >>= safeHalf
-- Just 10
This gives another useful way to understand Monad:
(>>=) ma f = join (fmap f ma)
where conceptually:
join :: Monad m => m (m a) -> m a
Failure becomes control flow
One of the nicest things about Maybe is that failure propagation becomes part of the context rather than boilerplate around every step.
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide x y = Just (x / y)
Then:
Just 5
>>= (`safeDivide` 0)
>>= safeRoot
returns Nothing. Once the chain reaches Nothing, subsequent Maybe computations are skipped.
Nothing short-circuits the remaining chain.Without Monad we might write:
case start of
Nothing -> Nothing
Just x ->
case safeDivide x 0 of
Nothing -> Nothing
Just y -> safeRoot y
Bind factors that repetitive plumbing away.
From bind to do
Haskell’s do notation is mostly a pleasant syntax for chains of binds.
This:
program :: Maybe Double
program =
Just 20
>>= safeHalf
>>= (safeRoot . fromIntegral)
can be written:
program :: Maybe Double
program = do
half <- safeHalf 20
safeRoot (fromIntegral half)
The line:
half <- safeHalf 20
should not be read as ordinary assignment. It means, roughly:
run this contextual computation; if the context allows a value to emerge, call that value
halffor the rest of the chain.
data Tea = Assam | Darjeeling | Masala
deriving Show
data Biscuit = Marie | ParleG | Bourbon
deriving Show
chooseTea :: String -> Maybe Tea
chooseTea "assam" = Just Assam
chooseTea "darjeeling" = Just Darjeeling
chooseTea "masala" = Just Masala
chooseTea _ = Nothing
pairBiscuit :: Tea -> Maybe Biscuit
pairBiscuit Assam = Just ParleG
pairBiscuit Darjeeling = Just Marie
pairBiscuit Masala = Just Bourbon
teaTime :: String -> Maybe (Tea, Biscuit)
teaTime input = do
tea <- chooseTea input
biscuit <- pairBiscuit tea
pure (tea, biscuit)The second computation genuinely depends on which Tea value the first computation produced.Notice why this is more naturally Monad than Applicative:
biscuit <- pairBiscuit tea
The computation pairBiscuit tea cannot even be chosen until we know tea.
Applicative versus Monad
This distinction is worth keeping visible.
Applicative
Person <$> getName <*> getAge <*> getCity
The computations are independent. We know the whole structure in advance.
Monad
do
country <- getCountry
city <- getCityFor country
weather <- getWeatherFor city
pure weather
Each later computation can depend on an earlier result.
Composition in the world of effects
Ordinary functions compose beautifully:
(.) :: (b -> c) -> (a -> b) -> a -> c
But monadic functions have types such as:
f :: a -> m b
g :: b -> m c
Ordinary (.) cannot connect them directly, because f produces m b, while g wants a plain b.
Bind provides the missing bridge.
Haskell packages that bridge as Kleisli composition:
(>=>) :: Monad m
=> (a -> m b)
-> (b -> m c)
-> (a -> m c)
So:
safeHalfD :: Double -> Maybe Double
safeHalfD x
| x >= 0 = Just (x / 2)
| otherwise = Nothing
pipeline :: Double -> Maybe Double
pipeline = safeHalfD >=> safeRoot
The laws are not mystical either
A Monad obeys laws that ensure sequencing behaves predictably.
Using pure and >>=:
pure a >>= f == f a
m >>= pure == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)
These are called left identity, right identity and associativity.
Changing only the parentheses in a chain of binds must not change the meaning of the computation.
Proof
The law ensures that sequencing is structurally stable. Whether we first combine m with f, or first describe how f continues into g, the same contextual computation results. This is what makes longer pipelines refactorable without changing their semantics.
The simplest useful picture
If you remember only one contrast, remember the shapes of the functions:
Functor a -> b
Applicative f (a -> b)
Monad a -> f b
Functor lifts a transformation.
Applicative combines independent contextual computations.
Monad chains contextual computations when the next one depends on the value produced by the previous one.
That is already enough to remove most of the mysticism.
What is the result?
Just 8 >>= safeHalf >>= safeHalfReveal solution
safeHalf 8 gives Just 4, then safeHalf 4 gives Just 2, so the complete expression evaluates to:
Just 2Rewrite this nested case expression using do notation:
case chooseTea "masala" of
Nothing -> Nothing
Just tea ->
case pairBiscuit tea of
Nothing -> Nothing
Just biscuit -> Just (tea, biscuit)Reveal solution
teaTime :: Maybe (Tea, Biscuit)
teaTime = do
tea <- chooseTea "masala"
biscuit <- pairBiscuit tea
pure (tea, biscuit)