☕ biscuits in teaProgrammingMonads Without Mysticism↑ Top
Haskell · Programming

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.

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)
TikZ pipeline showing Just 20 bound to safeHalf to produce Just 10, then bound to safeRoot to produce Just 3.16.
Bind is a sequencing operator: each stage may inspect a value and return a fresh contextual result.

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.

TikZ diagram showing a Maybe computation becoming Nothing after safeDivide 0 and a later safeRoot step being skipped.
In the Maybe Monad, 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 half for the rest of the chain.

TeaOrder.hs
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
TikZ diagram of Kleisli composition connecting a function a to m b with a second monadic function to produce a to m c.
Kleisli composition restores the compositional feel of ordinary functions for functions that produce effects.

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.

Monad associativity, intuitively

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.

Exerciseeasy

What is the result?

Just 8 >>= safeHalf >>= safeHalf
Reveal solution

safeHalf 8 gives Just 4, then safeHalf 4 gives Just 2, so the complete expression evaluates to:

Just 2
Exercisemedium

Rewrite 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)