-
Notifications
You must be signed in to change notification settings - Fork 4
0106: Email Tutorials ‐ Monads III
AFP 9 – Monads III – Graham Hutton
- Email 600 – Where We Are in the Monad Journey
- Email 601 – Refresher: The Monad Class
- Email 602 – Refresher: What the State Monad Models
- Email 603 – State Transformers Revisited
- Email 604 – Revealed vs Hidden Information
- Email 605 – Two Function Types Compared
- Email 606 – The “Under the Waterline” Model
- Email 607 –
returnfor the State Monad (Revisited) - Email 608 –
bindfor the State Monad (Revisited) - Email 609 – Why Bind Hides State Plumbing
- Email 610 – The Tree Relabeling Problem
- Email 611 – Defining a Tree Type
- Email 612 – What “Relabeling” Means
- Email 613 – Manual State Threading (The Hard Way)
- Email 614 – Why Manual State Threading Is Error-Prone
- Email 615 – Recognizing a State Transformer
- Email 616 – Fixing the State Type
- Email 617 – A Fresh Label Generator
- Email 618 – Relabeling Trees with the State Monad
- Email 619 – Why the Monadic Version Is Better
- Email 620 – Extracting a Pure Interface
- Email 621 – What We Have Learned
- Glossary
- Quiz Questions (Table Format)
- Quiz Options (Table Format)
- Next Steps
So far in our monad journey, we have completed two major steps. First, we rediscovered the idea of monads by studying concrete examples. Second, we learned the formal definition of monads in Haskell.
We have already seen three important monads:
- The Maybe monad, which models failure.
- The List monad, which models nondeterminism.
- The State monad, which models computations that manipulate state.
In this lecture, we return to the State monad, not to redefine it, but to use it in practice.
The Haskell definition of a monad fits on a single slide:
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m aThis definition tells us two important things.
First, every monad must already be an applicative. Second, a monad provides a generic sequencing operator, called bind.
The State monad models computations that carry and modify state, while remaining purely functional.
A state is simply some value that may change over time, such as a counter or a label generator.
Crucially, no mutation is used. Instead, state is passed explicitly—though usually hidden from us.
A state transformer takes an input state and produces:
- A result value
- A possibly modified state
This is captured by the type:
State -> (a, State)To turn this into a monad, we wrap it using a newtype:
newtype ST a = S (State -> (a, State))This allows us to define Functor, Applicative, and Monad instances.
One of the most important ideas in this lecture is the distinction between:
- What the programmer sees
- What the monad manages behind the scenes
The State monad hides state manipulation, allowing us to focus only on the meaningful data flowing through the computation.
Consider these two function types:
Char -> Int
Char -> ST IntThe first is a pure function. It takes a character and returns an integer.
The second looks similar, but it hides something important.
A function of type:
Char -> ST Intis not just mapping characters to integers.
Under the hood, it is equivalent to:
Char -> State -> (Int, State)The state flows underneath, hidden from view. This leads to a powerful mental model:
Values flow above the waterline. State flows below the waterline.
The programmer sees the values. The monad handles the state.
The return function lifts a value into the State monad:
return x = S (\s -> (x, s))This means:
- The value
xis returned unchanged. - The state is passed through unchanged.
Above the waterline, the value flows straight through. Below the waterline, the state remains untouched.
The bind operator sequences two stateful computations:
st >>= f =
S (\s ->
let (x, s') = app st s
in app (f x) s'
)What happens here is simple but powerful:
- Run the first computation on the state.
- Extract its result and updated state.
- Feed the result into the next computation.
- Continue with the updated state.
All state threading happens below the waterline.
When using >>=, the programmer does not manually pass state around.
The state flows automatically from one computation to the next. This eliminates boilerplate and prevents subtle bugs.
We now turn to a concrete example: relabeling trees.
The goal is simple:
Replace every leaf value in a tree with a fresh, unique integer.
We use a simple binary tree with values in the leaves:
data Tree a
= Leaf a
| Node (Tree a) (Tree a)This structure is recursive and ideal for demonstrating stateful traversal.
Given a tree with values like A, B, and C, we want to produce:
- A tree of integers
- Each integer is unique
- Labels increase as we traverse the tree
Without monads, we must write a function like:
Tree a -> Int -> (Tree Int, Int)This function must:
- Accept the next fresh label
- Return the relabeled tree
- Return the next unused label
Every recursive call must pass labels carefully.
Manual state threading leads to:
- Complicated plumbing code
- Hard-to-read definitions
- Easy mistakes (such as reusing labels)
Even a single wrong variable can break correctness.
The key insight is this:
The manual relabeling function already is a state transformer.
Once we recognize this, the solution becomes obvious: use the State monad.
We now make the state concrete:
type State = IntThe state represents the next fresh label.
We define a helper state transformer:
fresh :: ST Int
fresh = S (\n -> (n, n + 1))This returns the current label and increments the state.
Above the waterline: we get a number. Below the waterline: the counter advances.
Now the relabeling function becomes beautifully simple:
mLabel :: Tree a -> ST (Tree Int)
mLabel (Leaf _) = do
n <- fresh
return (Leaf n)
mLabel (Node l r) = do
l' <- mLabel l
r' <- mLabel r
return (Node l' r')There is no explicit state plumbing.
This version is:
- Shorter
- Safer
- Easier to understand
- Structurally identical to the problem description
The State monad handles the hard parts for us.
To expose a clean API, we define:
label :: Tree a -> Tree Int
label t = fst (app (mLabel t) 0)This is a pure function, with no visible state.
The State monad allows us to:
- Write imperative-style logic
- Without mutation
- With explicit sequencing
- While keeping pure function types
This is why it is one of the most important monads in functional programming.
State monad – models stateful computations in a pure way
State transformer – State -> (a, State)
Fresh value – a value guaranteed to be unique
Bind (>>=) – sequences computations while hiding state
Above the waterline – visible values
Below the waterline – hidden state plumbing
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: