Björn Devams

Björn

May 4, 2024

What the heck is a Monad?

What the heck is a Monad?

So back in the days, I remember that some of my colleagues where talking about Monads. During that time I had no idea what a Monad was and where the heck I needed it for? However, I agreed to myself to dive into this magical world. During this 'investigation' I listened to a talk from Douglas Crockford. One of the first things he sad was that once someone learns what Monads are and how to use them, they loose the ability to explain it to other people. So I thought by myself, in order to counter this I should write a blog about it. In order to get the most out of this blog it is important to have some experience with Scala. That being sad grab yourself a coffee or a nice cup of tea and enjoy the ride!

A Monad?

First of all, there is no such thing as a Monad type class in Scala. This doesn’t mean that there are no Monads in Scala. If you are familair with Scala but not with Monads. I will bet you already used Monads without knowing it. Monad is not a class or a trait. Monad is a concept.

According to Haskell a "Monad is a way to structure computations in terms of values and sequences of computations using those values" (www.wiki.haskell.org).

This is still a pretty global definition so we will dive a bit deeper into it. You can think of a Monad as a specific context where values or objects are living in. For instance, we can lift an Int value into an Option or a List context. When we lift a value inside an Option or List we can do extra operations on it like map and flatMap. You might think does this mean that Option and List are Monads? The answer to this question is yes, because Monads provide use with the following operations:

trait M[A] {
  def unit[A](a: A): M[A]

  def map[A, B](fa: M[A])(fn: A => B): M[B]

  def flatMap[A, B](fa: M[A])(fn: A => M[B]): M[B]
}

If you read our previous blog about Functors (refer to blog Anton) you might notice at this point that a Monad is almost the same as a Functor. The only difference is that a Monad also has a flatMap function. Hence, all Monads are also Functors. In contrast, a Functor is not always a Monad. That being sad lets look a bit closer to the specified functions above.

The unit method above can be seen as a constructor. It allows us to lift a value into a Monadic context. In addition, the map lifts a pure functions that operates on a type A and returns a B in a Monad. Hence, it represents sequencing a computation that does not introduce a new Monadic context. Last but not least, the flatMap is an upgraded version of the map. In contrast to the map function, the flatMap allows us to chain operations together that introduces a new Monadic context (Advanced Scala with Cats, page 84). Hence, the most important feature of a Monad is its ability to do sequecing computations.

Every value wrapped in a Monad becomes an object equipped with the aboved specified functions.

Monads in practice

Now that we know what Monads are and that its most import feature is its abiltiy to do sequential computations we can dive a bit deeper into it by means of some code examples.

Imagine that we receive two numbers. We need to calculate the sum of these numbers, pretty simple right? However, the numbers are optional. Meaning that we are not sure if we receive them. In order to tackle this we need to do some sequential computions. First of all, we need to check if both values exist. Secondly, if both values exist we need to sum them. In Scala, an Option is a Monad that might contain a value (i.e., Some or None). Hence, they could be a perfect fit for our example.

When we look into the unit, map and flatMap implementation of the Option inside the Scala library we see why this Monad is a perfect fit.

def apply[A](x: A): Option[A] = if (x == null) None else Some(x) //in our Monad trait above we called this unit
final def map[B](f: A => B): Option[B] = if (isEmpty) None else Some(f(this.get))
final def flatMap[B](f: A => Option[B]): Option[B] = if (isEmpty) None else f(this.get)

Wrapping our values inside the Option Monad allows us to execute the above specified map and flatMap functionality. We can give this map and flatMap our 'summing function' as a parameter to apply the suming functionality when both values are defined. So lets do this:

val number1: Option[Int] = Option(10)  // executes the apply method specified above
val number2: Option[Int] = Option(20)  // executes the apply method specified above

val mapResult = number1.map(x => number2.map(y => y + x))
//mapResult: Option[Option[Int]] = Some(Some(30))

val flatMapResult = number1.flatMap(x => number2.map(y => y + x))
//flatMapResult: Option[Int] = Some(30)

As described the map and flatMap functionality allows us to specify a sequence of operations that happens one after another. The main difference between both is that map allows us to chain operations together that does not introduces a new Monadic contex. In contrast, the flatMap allows use to introduce a new Monadic contex. As you see doing the sequencing computation with the map functionality changes the inner values of the Monad, but leaves the Monadic context intact. As a result it returns a value of type: Option[Option[Int]]. In contrast, the flatMap creates a new Monadic context where it squashes the Monad types together and returns a value of type: Option[Int].

Conclusion

From a programming perspective understanding what a Monad is, is not very difficult. Understanding this concept allows us to understand our code better. In addition, it can help us to write better code. However, this is not all there is about Monads. When you dive a bit deeper into Monads you will probably find the sentence: "Monads do not compose". Diving a bit more into this sentence ends you up with Monad Transformers. What this sentence means and what Monad Transformers are will be a subject in an upcoming blog. Stay tuned!