-
Notifications
You must be signed in to change notification settings - Fork 4
0104 : Email Tutorials – Monads I: Basic Concepts (Full Tutorial)
- Email 700 – Where We Are: From Functors to Applicatives
- Email 701 – Programming with Effects Revisited
- Email 702 – A Simple Expression Language
- Email 703 – A Naive Evaluator (and Its Problem)
- Email 704 – Introducing Failure with
Maybe - Email 705 – A Safe Division Operator
- Email 706 – An Explicitly Safe Evaluator
- Email 707 – Why the Explicit Version Is Unsatisfactory
- Email 708 – Observing a Repeated Pattern
- Email 709 – Abstracting the Pattern: The Bind Operator
- Email 710 – The Type of Bind
- Email 711 – Rewriting the Evaluator Using Bind
- Email 712 – Understanding Bind as Sequencing
- Email 713 – The General Bind Pattern
- Email 714 –
doNotation: Cleaning Up the Syntax - Email 715 – The Final Evaluator Using
doNotation - Email 716 – Why This Is Still Pure Functional Programming
- Email 717 – What We Have Really Discovered
- Glossary
- Quiz Questions (Table Format)
- Quiz Options (Table Format)
- Next Steps
Over the last few lectures, we have gradually built up a hierarchy of abstraction for programming with effects.
We started with functors, which let us map a function over values inside a data structure. Then we moved to applicative functors, which let us apply pure functions to effectful arguments.
At this point, we know how to write expressions such as:
pure (+) <*> Just 1 <*> Just 2This applies a pure function to arguments that may fail, and the applicative machinery handles the effects for us.
However, applicatives still have a limitation: they assume that the structure of the computation is fixed in advance.
The core question of this lecture is:
What if later computations depend on the results of earlier ones?
Applicatives cannot express this kind of dependency, because all arguments must already be present. To solve this problem, we need a new abstraction—one that allows sequencing, where the next step can depend on the value produced by the previous step.
Rather than defining this abstraction immediately, we will rediscover it through a concrete example.
We begin by defining a very small expression language.
data Expr
= Val Int
| Div Expr ExprAn expression is either:
- a literal integer value, or
- the division of one expression by another.
For example:
e = Div (Val 6) (Val 3)represents the arithmetic expression 6 / 3.
We now write a simple evaluator for expressions.
eval :: Expr -> Int
eval (Val n) = n
eval (Div x y) = eval x `div` eval yThis definition is elegant and concise. Unfortunately, it has a serious flaw: division by zero.
If eval y evaluates to 0, the program crashes. In real programs, crashing is unacceptable—we want to handle errors safely.
We already know a type that captures the idea of failure: Maybe.
The type Maybe a represents a computation that may either:
- succeed with a value
Just a, or - fail with
Nothing.
To handle division by zero safely, we will refine our evaluator so that it returns a Maybe Int instead of an Int.
First, we define a safe version of integer division:
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv n m = Just (n `div` m)This function never crashes. Instead, it explicitly represents failure using Nothing.
We now rewrite the evaluator using Maybe.
eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Div x y) =
case eval x of
Nothing -> Nothing
Just n ->
case eval y of
Nothing -> Nothing
Just m ->
safeDiv n mThis evaluator is correct and safe. It never crashes.
Although correct, this version has several problems:
- It is verbose and repetitive.
- Failure propagation is handled manually.
- The core idea of the computation is obscured by error-handling code.
Conceptually, the evaluator still does something very simple:
Evaluate
x, evaluatey, and then divide the results.
But this simplicity is hidden by layers of case analysis.
If we look carefully at the code, we see the same pattern appearing again and again:
case mx of
Nothing -> Nothing
Just x -> f xThis pattern says:
- If a computation fails, propagate the failure.
- If it succeeds, pass the result to the next computation.
This pattern occurs every time we sequence two Maybe computations.
Since this pattern appears so frequently, we should abstract it out into a single operator.
We define an operator—commonly called bind:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= _ = Nothing
Just x >>= f = f xThis operator captures exactly the repeated pattern we observed.
The type of bind tells us a lot:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe bInterpretation:
- Take a computation that may fail.
- If it succeeds, pass its result to a function that returns another computation.
- If it fails, propagate the failure automatically.
Bind gives us sequencing with failure propagation.
Using bind, we can dramatically simplify the evaluator.
eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Div x y) =
eval x >>= \n ->
eval y >>= \m ->
safeDiv n mThis definition is concise, readable, and directly expresses the intent of the computation.
The bind operator lets us write code that reads procedurally:
- Evaluate
x. - If successful, evaluate
y. - If successful, divide the results safely.
At no point do we manually check for failure. That logic is handled entirely by (>>=).
The general pattern of bind-based programming looks like this:
m1 >>= \x1 ->
m2 >>= \x2 ->
...
mn >>= \xn ->
f x1 x2 ... xnThis sequence succeeds only if every step succeeds. If any step fails, the entire computation fails automatically.
Because this pattern is so common, Haskell provides special syntax called do notation.
The previous code can be rewritten as:
do
x1 <- m1
x2 <- m2
...
f x1 x2This notation hides the lambdas and bind operators, making the code easier to read.
Using do notation, the evaluator becomes:
eval :: Expr -> Maybe Int
eval (Val n) = Just n
eval (Div x y) = do
n <- eval x
m <- eval y
safeDiv n mThis code closely mirrors the original unsafe evaluator, but it is now completely safe.
Although the code looks imperative, it is still:
- pure,
- declarative,
- referentially transparent.
There is no mutation, no hidden state, and no exceptions. Effects are represented explicitly in the type system.
We have not yet formally defined monads—but we have effectively rediscovered them.
A monad is precisely a structure that supports:
- embedding values (
return/pure), - sequencing computations with dependency (
>>=).
In the next lecture, we will formalize this idea and define the Monad typeclass.
Effect – extra computational behavior such as failure, state, or nondeterminism.
Bind (>>=) – sequences computations while managing effects automatically.
Maybe monad – models computations that may fail.
Sequencing – executing computations one after another, where later steps depend on earlier results.
do notation – syntactic sugar for chains of bind operations.
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: