1. Overview

As developers, we often encounter the need to encapsulate values with some additional context that cannot be directly related to the business domain. For example, we often need a value to be in a sequence or computed asynchronously.

Monads are the mechanism we can use to fulfill such needs. Let’s understand them by looking at some examples.

2. What Is a Monad?

Monads are nothing more than a mechanism to sequence computations around values augmented with some additional feature. The concept of monads comes directly from mathematics, precisely from category theory. For this reason, it’s often considered a tough topic. However, we’re going to make it as easy as possible.

As we said, a monad augments a value with some additional features. Such features are called effects. Some well-known effects are managing the nullability of a variable or managing the asynchronicity of its computation. In Scala, the corresponding monads to these effects are the Option[T] type and the Future[T] type.

As we can see, both types – Option and Future – define a type parameter. In fact, a monad adds an effect to a value wrapping it around a context. In Scala, one way to implement a monad is to use a parametric class on the type.

For example, let’s try to add to values of a generic type T the effect of laziness using monads. Hence, we define the Lazy[A] class as:

class Lazy[+A](value: => A) {
  private lazy val internal: A = value
}

The Lazy type wraps a value of type A, provided by a “call-by-name” parameter to the class constructor to avoid eager evaluation.

2.1. Wrapping Values Inside a Monad: the Unit Function

First, monads must provide a function that allows wrapping a generic value with the monad’s context. We usually call such a function unit. It’s said that the unit function lifts the value in the monadic context. In Scala, we can use the apply method of a companion object to implement the unit function:

object Lazy {
  def apply[A](value: => A): Lazy[A] = new Lazy(value)
}

In our example, the use of the unit function allows us to add the effect of the lazy initialization to a value, wrapping it inside the Lazy context:

val lazyInt: Lazy[Int] = Lazy {
  println("The response to everything is 42")
  42
}

Hence, the above code doesn’t print anything once executed because its execution’s laziness is lifted to monadic value.

2.2. Sequencing Computations Over a Value: the flatmap Function

However, the sole capability to add an effect to a monad isn’t worth the complexity added to the code. Moreover, we don’t want to extract the monad’s wrapped value to apply functions to it. It’s cumbersome and unmaintainable. We need a mechanism to sequence computations over a value wrapped inside a monad.

To overcome this problem, monads must provide the flatMap function. This function takes as input another function from the value of the type wrapped by the monad to the same monad applied to another type:

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

It transforms the value inside a monad into another value without performing any extraction to make it simpler. Hence, if we need to transform the lazyInt value into a String, we can use the flatMap function:

val lazyString42: Lazy[String] = lazyInt.flatMap { intValue =>
  Lazy(intValue.toString)
}

Once again, no string will be printed to the standard output because of all the computation’s laziness. Moreover, we changed the value inside the monad without extracting it. So, any chain of flatMap invocation lets us make any sequence of transformations to the wrapped value.

2.3. Make It More Imperative: Using the For-comprehension

Last, but not least, we can define the map function for any monad in terms of the flatMap function:

def map[B](f: A => B): Lazy[B] = flatMap(x => Lazy(f(x)))

Why should we do that? Because for any type providing both the map and the flatMap functions in Scala, we can use the for-comprehension construct (see the article A Comprehensive Guide to For-Comprehension in Scala for further details). The for-comprehension is quite useful for concatenating computations on the same monad:

val result: Lazy[Int] = for {
  first <- Lazy(1)
  second <- Lazy(2)
  third <- Lazy(3)
} yield first + second + third

As we can see, the above code is straightforward to read and lets us use the functional programming and monads in Scala as if we were coding using an imperative style.

For the sake of completeness, the above for-comprehension translates to calling the following sequence of functions:

val anotherResult: Lazy[Int] =
  Lazy(1).flatMap { first =>
    Lazy(2).flatMap { second =>
      Lazy(3).map { third =>
        first + second + third
      }
    }
  }

It’s a terrific improvement, isn’t it?

3. The Awful Three: Monads’ Laws

However, with great power comes great responsibility. In fact, it’s not sufficient to add the unit and the flatMap functions to a type to make it a monad. The complex part comes with the mathematical laws the monad must fulfill.

The three monad laws are:

  • Left identity
  • Right identity
  • Associativity

If the monad satisfies the three laws, then we guarantee that any sequence of applications of the unit and the flatMap functions leads to a valid monad — in other words, the monad’s effect to a value still holds.

Monads and their laws define a design pattern from a programming perspective, a truly reusable code resolving a generic problem.

Let’s describe them one by one.

3.1. Left Identity

The first of the three laws, called “left identity”, says that applying a function f using the flatMap function to a value x lifted by the unit function is equivalent to applying the function f directly to the value x:

Monad.unit(x).flatMap(f) = f(x)

If we take our Lazy monad, we have to prove that the following holds:

Lazy(x).flatMap(f) == f(x)

Hence, we can substitute flatMap(f) with f(x), so the property holds by definition.

3.2. Right Identity

The second monadic law is called “right identity”. It states that application of the flatMap function using the unit function as the function f results in the original monadic value:

x.flatMap(y => Monad.unit(y)) = x

It’s easy to prove that our Lazy monad also fulfills this law:

Lazy(x).flatMap(y => Lazy(y)) == Lazy(x)

As we can substitute the term flatMap(y => Lazy(y)) with the result of the application, Lazy(x), the property holds by definition.

3.3. Associativity

The last of the three monadic laws is the hardest to deal with and is called “associativity”. This law says that applying two functions f and g to a monad value using a sequence of flatMap calls is equivalent to applying g to the result of the application of the flatMap function using f as the parameter:

x.flatMap(f).flatMap(g) = o.flatMap(x => f(x).flapMap(g))

Hence, for the Lazy monad, the above rule becomes:

Lazy(x).flatMap(f).flatMap(g) == f(x).flatMap(g)

If we apply the substitution derived from the”left identity” law to the terms Lazy(x).flatMap(f) of the right side of the equation, we obtain exactly f(x).flatMap(g). So, the associativity rule also holds for our monad 🙂

4. Bonus Methods

Until now, we presented the minimum set of methods that a monad must implement to adhere to the pattern. However, we can define many other methods to improve the usability of the Lazy type.

For example, we can implement the flatten function that removes a level of abstraction in nested monads’ types:

def flatten(m: Lazy[Lazy[A]]): Lazy[A] = m.flatMap(x => x)

Moreover, another interesting function to implement is the one that extracts the value contained in the monad. We can call it get:

def get: A = internal

When we call the get function, the lazy value is finally evaluated, performing any effect previously enclosed in the monad.

5. Conclusion

In this article, we introduced the concept of monads in Scala. We began by giving a simple definition of monads, and then we introduced the minimum set of functions that a monad must implement: the unit and the flatMap. Finally, we spoke about the three monad laws.

Finally, monads are a fascinating and useful concept that pervades many types in the Scala standard library. Option, Future, Either, and more or less all the collection types such as List, Tree, and Map, to name a few, are monads.

As always, the full source code of the article is available over on GitHub.

7 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.