Monad - Breaking down this dreadful word...

If you were ever exposed to functional programming or went through a programming language course, you surely must have heard this dreadful word: Monad. It's usually followed by other words like complicated, confusing, difficult to understand, lots of theory, etc.

I was more than once in that position, trying to understand what a monad is by reading lengthy articles only to find myself more confused than before.

But in all fairness, monad is a pretty easy concept to grasp, at least if you are looking for a practical explanation and don't care about the theory behind it so much.

After all...

So, what is a Monad?

Well, a monad is a concept that comes from category theory. It does have a lot of theory behind it, which I have to admit, it can be quite overwhelming and complicated. If you are interested in that, there are multiple resources online to learn more about it (a good one is this series of lectures on category theory).

But in practice, a monad is basically a type wrapper which represents a specific form of computation.
In other words, you use it to wrap other types in order to give them some additional context.

Let's write some code and try to expand on it as we go along.
Let's try and create a new monad called Container which can represent the result of an operation that can either return a value (in this case it will be NonEmpty) or return nothing (Empty).

sealed trait Container[+A] {
  def isEmpty: Boolean
}

case class NonEmpty[+A](value: A) extends Container[A] {
  override val isEmpty: Boolean = false
}

case object Empty extends Container[Nothing] {
  override val isEmpty: Boolean = true
}

Is that all? Wow, that was simple.

Err, not exactly…in order for a type wrapper to be considered a monad, it needs to provide two operations.

Pure

Monads need to provide a way to wrap a pure value of a type into the monad itself (in other words, to yield a monadic value).

This function can be found with different names, depending on the language/library you might be using, but the most common ones are: identity, pure or unit (in Scala), return (in Haskel).

So, we can extend our example above to include this method.

trait Container[+A] {
  def isEmpty: Boolean
}

object Container {
  def pure[A](value: A): Container[A] =
    if (value == null) Empty
    else NonEmpty(value)
}

case class NonEmpty[+A](value: A) extends Container[A] {
  override val isEmpty: Boolean = false
}

It is sort of a monad constructor if you will. This is the reason why in the above example I defined it in the companion object instead of the trait itself.

FlatMap

The second thing that a monad needs to provide is a way to compose functions that output monadic values (called monadic functions).

Well, this is what I'm talking about:

def flatMap[B](f: A => M[B]): M[B]

Basically, it needs to provide a function that receives another function f as a parameter which is applied in the wrapped type A and returns a monad of another type B.

This function is commonly named flatMap, but again, you will find it with different names, depending on the language and/or library you're using, e.g. bind, >>= (in Haskel).

trait Container[+A] {
  def isEmpty: Boolean
  def flatMap[B](f: A => Container[B]): Container[B] = this match {
    case NonEmpty(value) => f(value)
    case Empty           => Empty
  }
}

object Container {
  def pure[A](value: A): Container[A] =
    if (value == null) Empty
    else NonEmpty(value)
}

Hmmm, so as long as I have a type wrapper that provides two functions with the above signatures, I have a monad?

Not exactly. We talked about naming and signatures…but we never talked about the laws these functions should obey.

Monad Laws

In order for a type wrapper to be considered a proper monad, in addition to providing the pure and flatMap functions, it needs to obey certain laws as well, called monad laws.

In particular, there are 3 laws:

  • Left Identity: If we create a monad out of a value and then flatMap the monad using a function f, it should give us the same result as applying the function f in the initial value.
def onlyPositives(value: Int): Container[Int] = 
  if (value >= 0) NonEmpty(value) else Empty

val value: Int = 1337

Container.pure(value).flatMap(onlyPositives) == onlyPositives(value)
  • Right Identity: If we have a monadic value and we flatMap using the pure operation, we should get back the initial monadic value.
val monadicValue: Container[Int] = Container.pure(42)

monadicValue.flatMap(Container.pure) == monadicValue
  • Associativity: When we have a chain of monadic function applications, it shouldn't matter how they are nested.
val monadicValue: Container[String] = NonEmpty("monads rule")
def size(value: String): Container[Int] = Container.pure(value.length)
def isEven(value: Int): Container[Boolean] = NonEmpty(value % 2 == 0)

monadicValue.flatMap(size).flatMap(isEven) == monadicValue.flatMap(str => size(str).flatMap(isEven))

Conclusion

All the above could be summarised in the following:

  • A monad is a type wrapper that provides some computation context to the wrapped values
  • It needs to provide a way of wrapping values of any basic type within the monad (i.e. create monadic values)
  • It needs to provide a way to compose functions that output monadic values (i.e. monadic functions)
  • It needs to obey the three monad laws: left identity, right identity and associativity

Of course, in reality, monads will most likely have much more methods than the two I described above. But all these methods, can be composed in one way or another from the two listed here.

I hope, at this point, monad is a less scaring concept for you.

But, why do we even care about monads? Why all these laws and rules? And what are these free monads, comonads and additive monads you've been hearing about?

Well, let's talk about this and also give some concrete examples of monads that you can find in most functional programming languages in another post.