Now, Really, What's a Monad?: Explained for the rest of Us

The topic at hand comprises three terms that might sound greek for some of the readers, parser being probably the only to sound familiar. For those two terms, some of the readers might have some familiarity because they’re pretty much memes at this point, but, and stick with me, it’s not that deep, and by the end of this article, I hope you gain some familiarity with them.

What do you think this code does:

List(1, 2, 3).map(x => x + 1) // 1: List(2, 3, 4)
List(1, 2, 3).flatMap(x => List(x, x + 1)) // 2: List(1, 2, 2, 3, 3, 4)

You might be pretty familiar with the map (1) example, maybe not much with the flatMap one (2), yet still they belong to the same family of composition. For some people this what there is of Functional Programming. Chained calls that doesn’t mutate the values it operates on but create new ones, but actually this is a short-sighted, non-abstract way to look at it. But we’re programmers and computer scientists, we love abstractions. They give us the power to operate different kinds of constructs by using the same underlying principles, the same API.

But how will one go to abstract something like a map on a list, and moreover, why?

Let’s return to our previous example.

If you look a List[A] as something that has a value (or more, but this has nuance), map would be a way to operate (do something) over those values, while keeping the list structure, right? List[A] is something more than simply a value of type A. You just can’t sum a List[Int](1) + 1 even if the list contains just one value. List represents something that might as well contains zero values or more than one, but the compiler can’t just tell. The compiler only knows, and that’s completely right, that there is a List[Int] for which List[Int] + Int => Int is undefined.

Most of the time we’re happy to keep the list and toasting it around whenever we need, which means not losing what’s in there. Sometimes, however, we are happy to lose information, or use a specific item in a list to make an algorithm we need:

val list = List(1, 2, 3)
val head: Option[Int] = list.headOption // 1: Some(1)

val sillyProgram: List[Option[Int]] = list.map(x => head.map(h => h + x)) // 2: List(Some(2), Some(3), Some(4))

val lessSillyProgram: List[Int] =
    sillyProgram.collect:
        case Some(x) => x // 3: List(2, 3, 4)

If you feel difficulty has just ramped up, look again. We’re using the same stuff we use in the previous example, with the addition of sequence, which we’ll talk a bout later. Let’s follow through it step by step, although you might be familiar with some of the stuff:

  1. We take the head of the list as an Option[Int] data-type. Here option means something quite similar to List[?], in the sense a list can hold zero or more values, meanwhile option can only hold zero or one, making it perfect to represent something that might be or might be not absent.
  2. Then we map each of the values on list (this is how map behaves on lists) to the mapped value of our headOption, which might or might be not be there, giving us a list of options.
  3. Then we collect our results, those being of type Some[Int] and unapply it so we get the raw collection of Int values. (This will ignore any result of the data-type None.)

The goal of this example is to illustrate how multiple data structures, or how we call it for reasons I hope to explain later, ‘data-type’, can represent different kinds of computations and their values, but how we used the same API – a map, to extract those values, manipulate them, and put it them back.

In fact, this is the perfect time to remark something, which is that you don’t ‘extract’ the values inside those data-types, but just manipulate the value inside, but leaving the overarching structure as is, but now with a description of how to do said manipulation appended to it.

This might sound abstract, because it is, but that’s the magic of all this stuff!

So what is it really a Monad? Well, a monad is category. And what is category? Is sort an abstraction, a nebula that contains objects, and some relations between those objects. Take the List[?] as an example. List is a Monad. In its confines, it has objects – the objects on the list, but also define operations from those objects to others.

The Monad category, inside its nebula, contain a specific association between those objects, namely flatMap.

Both Option and List are Monads, so they its objects are mappable into another Monad, leaving the flatMapped monad, well, flatten, just as the (2) of the first example, where we return another list inside the mapping, but this list is then concatenated to the parent list, leaving a List[Int] instead of a List[List[Int]].

Ok, but, how does this matter? The key is to understand that this concept of having something that's pending some kind of evaluation to produce a value. To this abstraction we call it an Effect.

Monads are not the only kind of effect. There a plethora of them in order to combine effects. That is, each effect has a kind, and this kind determines what combinations or combinatorics we can use on them, in order to manipulate the objects they hold inside their nebula.

This diagram is from the Scala's Typelevel library Cats, which is a collection of categories for you to only use them (and profit), without having to remember every detail.

Categories down the line depends or inherit from the ones upwards. If you were be able to find the Monad category, I can point to you that it inherits from one called a Functor, which means that every Monad, apart from its flatMap capabilities, has the more known map combinator.

The canonical examples I've shown to you like Option[?] or List[?] have already implementations that resembles the Cats hierarchy, but Cats also provides actual implementations to their data-types for seamless integration with any data structures that implement them as well. You can implement your own!