Nauths · Practical uses of monads in Haskell
Antoine Leblanc
About<br>Home
Fr<br>En
Practical uses of monads in Haskell
2026-05-28
On the “haskellquestions” subreddit, a user recently asked for some help<br>with monads in Haskell. In their post, they wrote (slightly paraphrased):
I feel stuck. I understand the basic concept of monads, but when it comes to<br>the practical use of different types of monads, I am lost.
My answer to them was one long<br>comment. But,<br>in hindsight, I think that I could have structured my answer a bit differently,<br>I could have chosen better examples, I could have made that comment more<br>helpful. This post is an attempt at doing just so.
In it, we will explore how we can use some standard monads to structure our<br>code, what benefits they bring, and how to use them. In some ways, it covers a<br>lot of what a hypothetical “Haskell 103” would have been, back when I was<br>teaching Haskell at Google.
This post contains optional exercises, that you can expand if you want to test<br>your understanding of each section!
Contents
Preamble<br>Do
Desugaring
Motivating example<br>Types
Goal
Capabilities<br>Function type
Naive implementation
Monads<br>Reader
Writer
State
Limitation
Composition<br>Amalgam
Transformers
Putting it all together
Conclusion
Preamble
This post’s intended audience is people who are in the same boat as that<br>original Reddit user: people who understand monads in Haskell in theory, but<br>struggle to use them in practice. Consequently, this post will not re-explain<br>basic Haskell syntax, and will assume familiarity with it. More importantly, we<br>will not be covering what monads are. This post is not yet another monad<br>tutorial.
In other words, this should make sense to you:
class Applicative m => Monad m where<br>(>>=) :: m a -> (a -> m b) -> m b
Ok, so Monad is the trait of types that provide the chaining operator >>=<br>often referred to as andThen in other languages. Great. Now what can we do<br>with that?
Do
Even if this post doesn’t re-explain monads, it’s worth going over do<br>notation. do notation is one of the direct practical benefits of monads; it’s<br>an essential part of the syntax of the language, that desugars to calls to<br>>>=. To demonstrate its convenience, let’s use a slightly contrived example<br>that my former students will remember. Let’s imagine that we want to chain<br>operations in Maybe; that is, operations that can fail to return a result,<br>like a lookup for instance. If we imagine that we have access to those three<br>functions:
lookupTransaction :: TransactionID -> Maybe Transaction<br>lookupCustomer :: Transaction -> Maybe UserID<br>lookupUser :: UserID -> Maybe User
we could build one new big function that, given a TransactionID, gives us the<br>Maybe User of the corresponding customer if it exists.
Without using monads at all, such a function could be written like this:
lookupTransactionCustomer :: TransactionID -> Maybe User<br>lookupTransactionCustomer tid =<br>case lookupTransaction tid of<br>Nothing -> Nothing<br>Just transaction -><br>case lookupCustomer transaction of<br>Nothing -> Nothing<br>Just uid -><br>lookupUser uid
It’s not very complicated, but it’s clunky: at every step, we must check whether<br>we got Just a value, and “return early” if we didn’t and got Nothing. It’s<br>like Go’s if err != nil { return err; }, but with staircases. Surely we can do<br>better.
Obviously we remember that Maybe is a monad! The nitty-gritty details of how<br>to chain operations have already been implemented for us, we don’t have to do<br>all those case _ of manually. Rewriting this function to make use of >>=<br>gives us the following:
lookupTransactionCustomer :: TransactionID -> Maybe User<br>lookupTransactionCustomer tid =<br>lookupTransaction tid >>= \transaction<br>lookupCustomer transaction >>= \uid -><br>lookupUser uid
This is better, and not just because it’s more terse: each step performs a<br>lookup, and we chain into the next step as long as the previous one gave us a<br>value. But… it’s still a staircase. We can do better, by using the<br>aforementioned “do notation”.
Desugaring
The keyword do opens a block, in which whitespace is significant: newlines<br>separate “statements”. This allows us to write sequential code, step by step,<br>and have it translated back into the correct chain of monadic operations.
-- this do block<br>foo = do<br>a bar<br>baz<br>b qux<br>pure (a, b)
-- desugars to<br>foo =<br>bar >>= \a -><br>baz >>= \_ -><br>qux >>= \b -><br>pure (a, b)
Armed with this, we can now finally rewrite our example function in its most<br>readable form (if not its most concise):
lookupTransactionCustomer :: TransactionID -> Maybe User<br>lookupTransactionCustomer tid = do<br>transaction lookupTransaction tid<br>uid lookupCustomer transaction<br>lookupUser uid
The core thing to remember is that this notation desugars to >>=, meaning that<br>it works for any monad. We can write do blocks for expressions in Maybe,<br>for lists, for IO… and so on.
Exercise 1
Here’s a classic “hello world” example:
main :: IO ()<br>main = do<br>putStrLn "Please input your name:"<br>userName...