For the Love of Haskell and Monads

Teiva Harsanyi
13 min readMar 19, 2024

This post aims to explain the concept of monads while highlighting Haskell's elegance. You do not need to know the Haskell language to read this post.

Let’s first start with the beginning and discuss the concept of functors.

Functors

This is a closed box:

Of course, as with every closed box, you can’t really know what’s inside unless you open it. So let’s open it, and… surprise!

The box contains the answer to the Ultimate Question of Life, The Universe, and Everything (who could have thought with such a small box?).

Yet, sometimes a box can also be empty:

How do we map the box analogy to programming? A box can be any data structure surrounded by a context. Said differently, a wrapper or container type that adds additional information or behavior to the underlying data.

In this example, the blue box represents the possibility of an optional value, which In Haskell is denoted by the Maybe type. But the concept exists in many languages: Option in Rust, sql.NullString in Go, Optional in Java, etc.

Yet, other type of boxes also exist:

  • A box representing that the wrapped value can be either from one type or another. In Haskell, the Either type represents a value that is either Left or Right. In Rust, Result represents either a success or an error, etc.
  • More generally, most classic data structures we can think of such as a list, a map, a tree, or even a string. Those data structures can contain zero, one, or multiple values inside. For example, a string can be composed of zero, one, or multiple characters.

We know how to apply a function to a simple value; for example, applying a function that adds one to a given int:

The white square represents a function that takes an integer and adds one to it.

But what if we want to apply the same function to a value inside of a box? We could open the box, extract the value out of it, apply the function, and put the result back in a box:

Yet, in Haskell, we can use fmap to apply a function on a box directly; no need to perform all the steps ourselves:

fmap is used to apply a transformation function, here (+1), to the value inside of a box and put the result inside another box.

In this example, the box itself is called a functor. A functor is an abstraction that allows for mapping a function over values inside a context without altering the context itself.

Not altering the context is crucial; a functor is not similar to the box in Schrödinger’s experiment. In quantum physics, opening a box alters the state of what’s inside. Here this is not the case: the box with the value 42 remains identical. And that’s a good thing, right? The answer to life shouldn’t be changed just by looking at it.

Examine the Haskell code for this example:

If you’re unfamiliar with Haskell, let’s spend 30 seconds on this snippet. The first line defines the signature of fmapEx: a function that takes no input and produces a Maybe Int output. The second line represents the core logic of the function: applying the (+1) transformation function to Just 42 (Maybe is a box type, while Just 42 is an instance of this box with the value 42 inside).

As we said, a box can either contain a value or be empty, so what happens if we apply the (+1) transformation to an empty box? The result is also an empty box:

Applying (+1) to a non-existent value doesn’t give 1. You can’t increase your bank account balance if you don’t have a bank account; the same is true in Haskell.

Here’s the code for this new example:

Nothing means a Maybe box with no value inside.

We also said that the box analogy can be applied to other data structures; for example, a list of values. Therefore, we can reuse the same logic and apply the (+1) function to every list’s element:

Another kind of box, so another color.

Pretty handy, right? Instead of looping manually over each element and creating a new list, we can provide a transformation function to fmap and Haskell will handle the rest for us.

That’s the essence of functors: an abstraction representing something to which we can apply a function to the value(s) inside.

Yet, in the next section, we will see that functors are somewhat limited. Let’s now move on to applicatives.

Applicatives

What if instead of applying a transformation function to a box like this:

We wanted to apply a transformation function which was itself inside of a box:

In that case, using fmap and functors, that’s a compilation error:

Indeed, the fmap function only works if the transformation function is outside of any box.

But hold on… What’s the purpose of having a transformation function inside a box?

A few examples:

  • When we want to represent a situation in which a function is optional or may be absent and that we need to handle this possibility explicitly using Maybe.
  • When we want to handle a variable number of functions, we can put these functions in a list.

Now that we understand why functions inside boxes are a thing, let’s understand how to handle this case:

The solution is to switch to another type: applicative functors (also called applicatives).

In this case, we must use in Haskell the <*> operator with applicatives (sorry if it sounds cryptic):

Thanks to the <*> operator, we can now apply the (+1) function inside a Maybe applicative to the value inside another Maybe applicative.

A small note, have you noticed that we referred to <*> as an operator? In Haskell, an operator is also a function, but written in infix notation; meaning placed between its arguments:

Now what happens if we try to use <*> on two different applicative types? For example, a Maybe Int and a list of Int:

In this case, that’s a compilation error. Applicatives are also there for safety reasons; the context has to be the same to use the <*> operator. Yet, if the transformation function is inside a list as well, this time it works:

And do you want to know what happens if we have multiple transformation functions in the first box and multiple values in the second box? Haskell applies the combination of each transformation function on each value:

So that’s what an applicative is: another abstraction that allows for applying functions wrapped in a context to values in the same context.

One last thing, what if a transformation function remains outside of a box?

Should we put this function inside a box? Should we turn the applicative into a functor to apply fmap? None of these is required. We can use the <$> operator, basically the fmap version for applicatives:

It illustrates that an applicative is an extension of a functor as it can cover both cases:

  • If the function is outside a box, we use <$> (A and B being generic types):
<$> takes an A to B function, applies it to the value inside an applicative, and puts the result inside another applicative.
  • And if the function is inside a box, we use <*>:
<*> takes an A to B function inside an applicative, applies it to the value inside an applicative, and puts the result inside another applicative.

Yet, same as with functors, we will also see that there’s a limit to how applicatives are helpful. Now, it’s time to move on to the final boss: monads.

Monads

So far, we have tackled two kinds of transformation functions:

  • A function outside of any box:
  • A function inside of a box:

But what if a function takes a value outside a box, applies a transformation, and puts the result inside a box?

First, let’s talk about the how, as there are two ways to do it in Haskell. We can either use Just as we want to return a Maybe Int:

This function takes an x variable outside a box and puts the sum of x + 1 inside a Maybe Int box. But there’s a second alternative that does exactly the same thing, this time using return:

return is a function that wraps something (an int, a function, whatever) inside of a box. Thanks to type inference and the function signature, Haskell knows that return applied on (x + 1) should put this value inside a Maybe Int. We will come back later to the essence of what return is in Haskell.

Now, let’s move on to the why. Why does a function accept a value outside a box and return a value inside a box? For example, consider the case of a safe divide function:

This function accepts two Float, x and y, and returns a Maybe Float. Then, it uses pattern matching:

  • If the denominator is 0, it returns Nothing
  • Otherwise, it returns the result of x / y inside a Maybe Float box

divide illustrates a function accepting inputs outside any box and returning a value inside a box.

Now let’s get back to our initial problem, can an applicative work with a function returning a value inside a box? Let’s give it a try.

The Limitation of Applicatives

We will cover a concrete scenario. We want to implement a function that receives a person's age and name. We want to greet the person only if he’s over 18 years old. For example:

  • Providing “John” and 30 to our function should return a Maybe String box with “Hello John”:
  • Yet, with the age of 16, for example, is should return Nothing:

Let’s first introduce the two utility functions to validate the age and greet the person:

validateAge uses Haskell’s guards syntax (|), a notation to define functions based on predicate values:

  • If the age is above 18, we put it inside a Maybe Int
  • Otherwise, we return Nothing

Regarding the greet function, it concatenates “Hello ” and the person’s name (using the ++ operator).

Back to applicatives, one could be tempted to write it this way:

Yet, this code doesn’t even compile. Let’s understand why.

The first part of the expression (greet <$> (Just name)) is OK, as we have seen that the <$> operator can be used to apply a function outside of a box to the value inside of a box:

This part of the expression takes a String and produces a Just String:

The second part of the expression, validateAge age, is a Maybe Int:

Now, taking the whole expression, it gives us the following:

And this part doesn’t compile. Indeed, we discussed previously what kind of function is expected by the <*> operator:

In this case, we can’t provide the correct type expected by <*> for the transformation function. So, we can’t make it work with applicatives (at least easily). We need something else.

Monads to the Rescue

To solve our problem, we can use monads and introduce a new operator, >>=:

This operator (still a function written in infix notation) takes:

  • A value of type A inside a box
  • A function that transforms an A type into a B type inside a box

As a result, it produces a B type inside a box. For instance:

This example in Haskell:

This is the first use case of monads, applying a transformation function that returns a value inside a context to a value inside the same context type.

Now, let’s return to our problem (greeting if a person is over 18) and understand how to solve it using monads and the >>= operator:

Note that the code here uses a lambda function. A lambda in Haskell is an anonymous function using the \ notation. For example, \x -> x + 1, which increments its input x by 1. In the previous code, the lambda represents a function that takes an Int (because validateAge age is a Maybe Int) and returns a Maybe String.

This is what our code looks like:

There’s one small thing that we could be bothered about. The transformation function passed to >>= takes an Int but doesn’t use it. This is the purpose of _, to express that we want to ignore this parameter. Could we do better? Yes!

To solve the same problem without a clumsy lambda expression that doesn’t even use its input, we can use the do notation:

The behavior of this code is the same as the previous one when we used the >>= operator:

  • If validateAge age returns Nothing (age under 18), the whole function returns Nothing. The computation terminates line 3 without further evaluation.
  • Otherwise, it returns a string inside a Maybe.

Using the do notation, monadic expressions are written line by line. It may look like imperative code, but they’re just sequential, as each value in each line relies on the result of the previous ones and their contexts. do is used as a convenient way to sequence and compose monadic computations:

  • Sequence: takes any traversable data structure (e.g., a list or a tree) of monadic values and transforms it into a monadic value of the same data structure. For instance, [Just 1, Just 2, Just 3] into Just([1, 2, 3]):
  • Compose: the act of combining two or more monadic functions together to create a new monadic function.

Remember about return? We said previously that return was used to wrap a value inside a context. More specifically, return wraps the value inside a monad; it does not end the function execution.

For example, what if we want to use the Maybe String value after return (greet name)? In Haskell, we can use <- to bind the result of a monadic action to a variable:

Notice the multiple uses of return. As we said, in Haskell, return doesn’t stop the function execution; instead, it wraps a value inside a monad.

Wrap Up

In summary, a monad is a powerful abstraction that extends the capabilities of applicatives. It provides a way to sequence and compose actions while preserving their contexts.

In Haskell, leveraging tools such as the do notation, the <- operator to bind variables, or return to wrap values inside of monads allows developers to craft code that is not only concise but also remarkably powerful.

The concept of monads in Haskell goes beyond the limited scope of what we have discussed. Even I/O operations are encapsulated within monads. It allows impure actions (e.g., writing a file or reading a socket) to coexist within a pure functional framework. The monadic structure enables the sequencing and combination of I/O operation alongside any other monads.

Thank you for reading. This post wasn’t just about dissecting Haskell’s technical merits; it was also a tribute to its elegance.

Nowadays, beauty in programming is a rare concept, often pushed aside by the pursuit of efficiency. Yet, I do consider Haskell a beautiful language. Thanks to constructs like monads, imperative-style code can be reconciled with the purity and declarative nature of functional programming.

I’ve fallen in ❤️ with Haskell; could it be your turn?

Edit — March 2024

Someone associated with the Haskell Foundation told me that writing about monads was a “fatal error”. This person also sent me The “What Are Monads?” Fallacy. TL;DR of this post:

Attempting to learn how to use monads by understanding what they are is like asking “What is a musical instrument?” and then assuming once you know the answer to that, you’ll be able to play all of the musical instruments.

First of all, I am not expecting you to be able to use monads in your daily work solely with this post. Obviously, it would require practice; I think we can all agree with that. However, I am slightly upset that I was told that writing about a technical concept is an error… For instance, would it be wrong if a beginner in Go wanted to write about goroutines? Under the pretext that mastering concurrency requires practice, should I tell this person this is a mistake? I don’t think so, and it would go against what I believe should be a programming language community: inclusive, supportive, and encouraging knowledge-sharing, regardless of skill level.

Worst case scenario: if you think this post is unnecessary (and you have the right to believe it), then just don’t read it 🙂. Ultimately, it won’t change my opinion of the language itself, and if this post makes one person want to look at Haskell, that’s already a win.

Haskellfully yours, Teiva.

--

--

Teiva Harsanyi

Software Engineer @Google | The Coder Cafe newsletter (https://thecoder.cafe) | 📖 100 Go Mistakes author | 改善