Skip to main content
Upsolver Engineering

Monads Are Not That Complicated

·4 mins

An introduction for Scala developers #

If you’ve been using Scala for a while, you have been exposed to functors and monads. However, because of naming decisions in the Scala libraries, you may not be sure what these concepts refer to.

Consider these three values:

val opt: Option[Int] = None
val lst: List[String] = List("very nice")
val fut: Future[Double] = getValueFromElsewhere()

Functor #

What do those values have in common?

  1. Their types have what is called a type parameter. The concept is also known as generics in many languages. If you squint a bit, you can see them as some kind of container. A Future is a container that will eventually contain a value (unless it fails).
  2. Their types have a .map method. Using that method, you can apply a function to the values “contained” in opt, lst, or fut. The resulting container is of the same “shape” as the container in the input. For example, the result of will be a List of the same length as lst.

Each type that has such a .map method is an example of a Functor. There are many more functors, but the examples above appear in everyday Scala code.

For the sake of completeness, I will add that for something to be a Functor, its .map method has to respect some simple laws. Those laws ensure that you don’t get surprises. One of those laws requires that == x for all possible values of x.

These laws cannot be checked by the Scala compiler, so it’s possible to write something that looks like a Functor because it has a .map method, but it isn’t because it doesn’t respect the Functor laws. However, as long as you only use existing Functor types and don’t write functors by yourself, you need not worry about this.

Photo by Ricardo Gomez Angel, Unsplash license. Source

Monad: A special case of Functor #

To apply a function f using, the input type of f needs to be compatible with the type parameter of myVal. If you check the types of the expressions involved, you will see that the code won’t compile otherwise.

However, the return type can be anything you like. So what happens if the return type of f is itself a container of the type we’re mapping over?

val g1: Int => Option[String] = ???
val myOpt: Option[Option[String]] =

val g2: String => List[Int] = ???
val myLst: List[List[Int]] =

val g3: Double => Future[Double] = ???
val myFut: Future[Future[Double]] =

Of course what happens is that the resulting container is nested. In some cases this may be exactly what you want, but in practice there is often an obvious way to turn the nested containers back into an unnested one.

  • An optional value Some(Some(3)) may for your purposes be equivalent to Some(3). Similarly, the value Some(None) can be seen as equivalent to None.
  • A nested list List(List(1, 2), List(3, 4)) may be seen as equivalent to List(1, 2, 3, 4).
  • Two nested futures can be seen as equivalent to one (unnested) Future that completes after both the outer and the inner future have completed, and returns the result of the inner Future.

Again, I’m assuming you’ve used Scala for a while, so you will know that this is exactly what .flatten does.

This pattern of applying .map and then .flatten happens so often that there is a method for it, and unsurprisingly it is called .flatMap. Roughly speaking, things that have such a .flatMap method are examples of Monad.

Monads are special cases of functors, so every Monad is a Functor, but not every Functor is a Monad. Again, for something to be a Monad it needs to respect some laws of behavior (in particular, it needs to respect the Functor laws) but we won’t go into that here.

Is that all there is to it? #


The examples shown arise frequently, but there are many other types that are monads. Some of them are less intuitive, but the beauty of knowing that something is a Monad is that you know it has .flatMap and related methods, and due to respecting some basic laws of behavior they will do exactly what you expect.

Further reading: