August 31, 2026

Haskell for Hobos-- Monads

Understanding monads is a vital aspect of really understanding Haskell.

Don't be frightened by all these new words, functor, monad, and so on. Also don't be frightened by fancy new operators like <$>, <*> and such. All these things are really generalizations of good old "map" and lists, so keeping that in mind will be helpful.

First of all "Monad" is a typeclass not a type. Many things in Haskell are monads. (Sometimes it seems everything in Haskell is a monad.) Lists are monads. Maybe is a monad. IO is a monad.

Second of all, Monads are not restricted in some way to doing IO. Monads are quite important to IO given how IO works in Haskell, but it is a mistake to think that Monads are an "IO thing". It is also a mistake (and a more common one) to think that do notation is a special thing for IO. Do notation is most commonly used in connection with IO in most programs, but do notation is a general thing used to work with monads.

To avoid this generating or reinforcing this IO and monad confusion, it try my best to avoid talking about IO in this document. At least I limit my use of IO as a monad for the sake of illustration.

For the purpose of understanding monads, I find it useful to view the types that participate in the monad game as containers or boxes. Or to view the types as having a certain smell or flavor. We want to operate on the things in the boxes, keeping the smell.

Review

Taking a list an an example, consider a list of integers. Perhaps we want to apply a function that operates on integers to the list. That function demands an integer argument, but the list has the type of "list" so we can't directly apply the function. What are we to do?

To solve this problem for lists, we have "map". Understanding map is a vital first step to understanding all of this. However map only works on lists (and map can only apply a function that takes a single argument).

To generalize map, we invent functors which give us "fmap". We can use fmap in place of map to work on lists, but this doesn't really buy us anything. But we can use fmap to operate on the Maybe type and that is new and powerful.

A further generalization (and a bit of a step sideways) is an applicative (short for applicative functor). It allows us to take a function in a box and apply it some other thing in a box and get a new thing in a box. This allow an applicative to work with functions with more than one argument, which is the benefit over plain old functors.

On to monads

There are a multitude of essays that attempt to explain the concept of Monads. Some of the introductions scold you, telling you that "you already know and use this". This is baloney. I've been programming for decades in more languages than I can count, and I have never run across this before. Show me a monad in C or even Python without standing on your head to do so.

As with many things in Haskell, a big part of the challenge is dealing with a high level of abstraction. I think one of the best ways to deal with this is to show you these abstract things in action using examples.

Let's start by looking at the actual definition of the Monad typeclass. It looks something like this:

class Monad m where
	(>>=) :: m a -> (a -> m b) -> m b
	(>>) :: m a -> m b -> m b
	return :: a -> m a
I think the way to address a new typeclass is to take a look at what new operator it exists to introduce into the language. Here we have (>>=) and (>>). If we can figure out and/or illustrate how these work, we will understand monads.

Notice the "return" operator. I find this a bizarre choice of a name, that seems almost a diabolical attempt to confuse us because it is nothing like what return does in any other language. What return does is to simply wrap some basic type into a monad. I might have chosen the word "wrap" or "inject". It is exactly the same a "pure" in Applicatives.

The (>>=) operator (sometimes called "bind" or "chain") is the heart of the issue. It's first argument is some thing "a" wrapped as a monad. The second argument is a function, which takes an unwrapped "a" and gives us a wrapped "b". The operator yields the "b" wrapped as a monad.

It is not unlike "map" except here we have this awkward function that takes an unwrapped value, transforms it, and give it to us as a wrapped value. Now we need to see all this in action. Here is a silly demonstration:

lucky x = Just (x+13)
output = Just 100 >>= lucky

main = putStrLn $ show output
I am very proud of myself. This is the first time -- ever -- that I have used the >>= "bind" operator. We get the output "Just 113", which should not be a surprise. Let's take a look at this.

First I want a function that takes an ordinary thing and wraps it into a Monad. I choose the Maybe type as my monad and the function just adds 13 to the argument and wraps it using Just.

Next we concoct a Maybe monad using "Just 100" and then feed it to our function using the "bind" operator (>>=). This seemed to follow my understanding of things as described above, and much to my surprise it works. I'll note in passing that this "feels" like a pipeline. We are feeding a monad into a function and getting a monad out. We could continue in this way if we had another function

lucky x = Just (x+13)
greedy x = Just (x*100)
output = Just 100 >>= lucky >>= greedy

main = putStrLn $ show output
This gives us the output "Just 11300". It has been said that monads are (or can be) used for sequencing, and this indicates how this might be so.

Hold on now a minute! You may be thinking that now you know all there is to know about how the monad game works. But that ain't so. Remember that there is more to a Maybe than just wrapping a value inside of Just. Don't forget that we need to handle the value "Nothing". Let's see how that works.

trouble _ = Nothing
lucky x = Just (x+13)
greedy x = Just (x*100)
output = Just 100 >>= trouble >>= lucky >>= greedy

main = putStrLn $ show output
Here we invent the function "trouble" that ignores its input and always returns Nothing. When we calculate "output" it will ignore the "Just 100" it receives as input and return Nothing, which gets passed to lucky and then to greedy and we get the output:
Nothing
Here we have "monad magic" working for us. The idea of packing and repacking does not apply here. The monadic value of Nothing just has to pipeline its way through the functions lucky and greedy. The way this is handled is set up by the Maybe type, which is obliged to provide a definition of the >>= operator when it elects to advertise itself a member of the Monad typeclass.

Other types will need to define the operator in a way that is appropriate to make this magic work. A list is a monad. How does it define the >>= operator?

What about list as a Monad?

A bit of experimenting tells the tale:
bloat x = [ x, 10*x]
outlist = [ 1, 2, 3 ] >>= bloat

main = putStrLn $ show outlist
[1,10,2,20,3,30]
This returns [1,10,2,20,3,30] as shown. What goes on is that the function ("bloat" in this example) is applied to every element in the list. This yields a list of lists -- but that is then flattened into a single list and there you are.

Haskell has a function named "concatMap" to do just this (who would have known). The point here is that every type which is a Monad has its own particular implementation of >>=, hopefully designed with good sense.

Monads everywhere

Take a look at this:
comp = [1, 2] >>= \x -> return (x)
comp = [1, 2] >>= \x -> return x
No surprise here, this just returns the list [1,2]. But each element gets passed to the lambda, gets turned into a singleton list, which ends up in a list of singleton lists which gets flattened. The following requires more thought:
comp = [1, 2] >>= \x -> [10, 20] >>= \y -> return (x * y)
This yields [10,20,20,40]. A way to look at it is that the first list [1,2] gets its elements extracted and passed as "x" in the lambda. Each time that happens the second list is processed somewhat like an "inner loop".

What is particularly interesting is that this is equivalent to the following list comprehension:

[x * y | x <- [1, 2], y <- [10, 20]]
Certainly the syntax of the list comprehension is easier (for me at least) to understand.

A side note on return

We mentioned that return can be used to wrap a "plain old thing" was a monad. Why not use it instead of "Just" in the functions above? Somewhat to my surprise, it works:
lucky x = return (x+13)
I expect to have to declare the type of the function explicitly using something like:
lucky :: Num a => a -> Maybe a
lucky x = return (x+13)
This of course works, and is good practice. What saved us before being this explicit was Haskell's type inference. It saw that "lucky" was being used in an expression that involved the Maybe type and concluded that the "return" needed to produce such a thing.

Some monads are used simply for their side effects. (Yes, I know, Haskell is supposed to be pure and not have side effects). The usual example would be IO. In that case the return value of the monad would be irrelavant and could be almost anything. The convention in such cases is for the monad to yield the value (). Yep, and empty tuple. You will often see "return ()" used in such cases, which serves to even more strongly confuse and mislead people used to programming in other languages.

And what about that >> operator?

Should we call it a function or an operator? It is a function we are using as an operator. Remember the saying, "Everything is a function in Haskell."

Sometimes >> is called "then". It sequences two monadic actions together, just like >>= but it throws away the result of the first action. The following defines it in terms of >>=, which we think we understand.

k >> f = k >>= \_ -> f
This only works if the second action will not be looking for a result from the first. You may remember that Applicative had the *> operator that did the very same thing. We can use it in this situation:
main = putStrLn "Hello" >> putStrLn "World"
Here the second putStrLn is not looking for anything being passed to it from the first. The first just yields "IO ()" as a result, which we are perfectly happy to throw away. The end result is sequencing the two actions to get this output:
Hello
World
Sequencing is important in Haskell. Unless constrained to some order, Haskell generally feels free to execute functions that need to be executed in any order.

Do notation

Now that we understand that >>= and >> are all about sequencing operations, we can understand what do notation is all about. What do notation does is to hide all those scary >>= and >> characters and fool us into thinking we are writing imperative code!

Suppose we have 4 monadic operations we want to sequence. We can write:

a >> b >> c >> d
Or we could write the following (very commonly these are each putStrLn xxx). This is called "sequencing without binding".
do a
   b
   c
   d
Similarly, we could write:
a >>= (b >>= (c >>= d))
This is equivalent to:
do tmp1 <- a
   tmp2 <- b tmp1
   tmp3 <- c tmp2
   d tmp3
It is possible to use do notation for IO as a Haskell beginner without understanding or caring about any of this -- and it is fairly common. It is a good stepping stone perhaps, but will probably yield confusing surprises.

Do notation deserves a whole section of its own, which I will make every effort to provide elsewhere.

Some closing comments

I find myself saying, "is that all there is to it?". And it turns out that in fact yes, that is what monads are all about. Understand how >>= works and "you got it". >> is just a "crippled" (but useful) version of >>= that tosses the result from the first operation.

We can talk all day about things in container or boxes, but that won't get us any closer to understanding Monads (or any typeclass). The way to understand a typeclass is to take a close look at the operators that the typeclass introduces. In the case of Monads this is >>= (as well as >>).

Some monad essays

The "You could have" essay is well regarded and worth a look, especially if monads are starting to make sense after working through my presentation.


Have any comments? Questions? Drop me a line!

Tom's software pages / tom@mmto.org