I’ve been interested in Haskell and functional programming for quite a while now. The concept was first introduced to me at university and I’ll admit that most of it sailed over my head. My earliest programming experiences were exclusively imperative (VBScript, Visual Basic 6, Python, C) and even Object Oriented Programming felt like a giant leap of magic when I first encountered Java. I quickly changed my tune on that front, and Java became a mainstay in my toolbelt as an engineer.
Surprisingly, Java is also where I discovered my love for functional programming. Those Java developers among you may remember the Java 8 release in 2014, when lambdas, streams and Optional arrived1 and suddenly facilitated a much more functional style. This led to a phase of perhaps strangely functional Java code being written and considered best practice. That’s where I first started stretching my brain in that direction. Of course, if you want to write clean functional code without side effects, you’re better off reaching for a language designed around those ideas. And that’s where I fell in love with Haskell in all its strongly-typed, lazily-evaluated, side-effect free glory.

Learn You a Haskell for Great Good by Miran Lipovača was the book I picked up to learn and I’d highly recommend it, or alternatively the open source fork that’s been updated with new information in the 15 years since the original was released. After working through it I felt like I had a solid grasp of Haskell and the core FP concepts. Except for monads.

It’s almost a meme at this point. Beginners hit monads and bounce off, even though the underlying concept is remarkably simple. If you’re coming from an OOP background, the closest mental model is something like fluent interfaces or the Builder pattern: you chain operations together, and each step feeds its result into the next while some hidden machinery manages the context around those results.
You Google “what is a monad,” find seventeen blog posts (eighteen now, you’re welcome) comparing them to burritos, spacesuits or assembly lines, and come away more confused than when you started. The analogies fail because they obscure what’s actually a fairly mechanical idea. Monads are a design pattern for chaining operations together when those operations have some kind of context attached to their results. That context might be “this could fail,” or “this produces side effects,” or “this depends on some state.” The monad gives you a structured way to handle that context without drowning in boilerplate.
Start With a Problem, Not a Definition Link to heading
Forget Haskell for a moment. Think about a chain of operations in any language where each step might fail.
user = get_user(id)
if user is None:
return None
address = get_address(user)
if address is None:
return None
city = get_city(address)
if city is None:
return None
return city
Every step could return None. You end up with this staircase of null checks that obscures what you’re actually trying to do: get a user, then their address, then their city. The pattern you want is: “run this sequence of operations, but if any step produces nothing, short-circuit the whole thing.” That’s exactly what the Maybe monad does.
Wrapping Values in Context Link to heading
A monad wraps a value in some context. For Maybe, that context is: “this value might not exist.”
data Maybe a = Nothing | Just a
Just 42 means “I have a value, it’s 42.” Nothing means “there’s no value here.” Simple enough. But the power isn’t in the wrapper itself. It’s in how you chain operations on wrapped values.
The Key Operation: Bind Link to heading
The operation that makes a monad a monad is called bind (written >>= in Haskell). It takes a wrapped value and a function that operates on the unwrapped value, then returns a new wrapped value.
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
In English: “Give me a Maybe a and a function from a to Maybe b, and I’ll give you back a Maybe b.”
The implementation is dead simple:
Nothing >>= f = Nothing
(Just x) >>= f = f x
If the input is Nothing, don’t bother calling the function. Just propagate Nothing. If there’s a value inside, unwrap it and pass it to the function. Now that ugly Python staircase becomes:
getUser userId >>= getAddress >>= getCity
If any of them returns Nothing, the whole expression evaluates to Nothing. No null checks. No staircase.
The Three Laws Link to heading
For something to be a proper monad, bind has to behave predictably. There are three laws2:
Left identity:
return a >>= fequalsf a. Wrapping a value and immediately binding it to a function is the same as just calling the function on the value.Right identity:
m >>= returnequalsm. Binding a wrapped value to the wrapping function gives you back the same wrapped value.Associativity:
(m >>= f) >>= gequalsm >>= (\x -> f x >>= g). The order you group your binds doesn’t matter.
These aren’t rules you need to memorise. They just guarantee that chaining operations works the way you’d expect, with no weird surprises when you compose things together.
It’s Not Just Maybe Link to heading
The pattern generalises. Different monads handle different kinds of context:
Either handles operations that can fail with an error message, not just a silent Nothing:
parseJSON input >>= validateSchema >>= extractFields
If any step fails, you get a Left "error message" and the rest is skipped.
IO sequences operations that interact with the outside world:
getLine >>= putStrLn
Read a line from stdin, then print it. This matters because Haskell is otherwise free to evaluate things in any order it wants.
List handles non-determinism, where operations produce multiple results:
[1,2,3] >>= \x -> [x, x*10]
-- Result: [1,10,2,20,3,30]
State threads mutable state through a sequence of pure functions without actually mutating anything.
The shape is always the same. A wrapped value, a function that produces a new wrapped value, and bind handling the plumbing between them.
Do-Notation Link to heading
Haskell provides do-notation as syntactic sugar to make monadic code read more like imperative code. These two are equivalent:
-- With bind
getLine >>= \name ->
putStrLn ("Hello, " ++ name) >>= \_ ->
return ()
-- With do-notation
do
name <- getLine
putStrLn ("Hello, " ++ name)
return ()
The do version looks like a sequence of statements. Under the hood, it desugars into the same chain of >>= calls. This is why Haskell can look imperative when it needs to, without abandoning purity.
Why This Matters Outside Haskell Link to heading
You’ve already used monads without knowing it. JavaScript’s Promise.then() is essentially bind for asynchronous computations (it doesn’t strictly satisfy the monad laws due to auto-flattening, but the shape is the same). Rust’s ? operator is bind for Result. C#’s LINQ query syntax is do-notation for the list monad. Optional chaining (?.) in Swift, Kotlin and TypeScript is a limited form of Maybe bind.
Haskell just names the pattern explicitly and builds its entire effect system around it. Other languages use it selectively, often without the vocabulary.
The Burrito Problem Link to heading
Monad tutorials fail because they try to find one physical metaphor that covers all monads. But monads aren’t a thing, they’re a pattern. Saying “a monad is like a burrito” is as useful as saying “recursion is like looking into a mirror that’s facing another mirror.” If you already understand recursion, you nod along. If you don’t, you’re just more confused than before. Monad analogies work the same way: they only click once you no longer need them.
If you understand Maybe and how >>= short-circuits on Nothing, you understand monads. Everything else is just the same pattern with different plumbing.

Further Reading Link to heading
- Learn You a Haskell for Great Good3 has an excellent chapter that builds intuition gradually
- Typeclassopedia4 covers the full typeclass hierarchy (Functor → Applicative → Monad) with rigour
- Philip Wadler’s original paper Monads for functional programming5 is surprisingly readable for an academic paper
What’s New in JDK 8 - Oracle’s official release notes for Java SE 8, released March 2014. ↩︎
Monad laws - HaskellWiki’s formal specification of the three laws any monad instance must satisfy. ↩︎
Wadler, Philip. Monads for functional programming. 1995. ↩︎