Overview

Monads demystified

No Comments

In this short post I want to take a look at monads from a pragmatic perspective, i.e. why and how monads can be useful for developers. I won’t talk about any theory, but instead show code examples in Scala. I’ll even call things monad which don’t fully obey the monad laws as long as they are monadic enough to be useful (well known experts did that before, so I’m not afraid).

Here comes the use case for monads:

Monads are for sequencing dependent contextual computations.

Let’s dissect this statement step-by-step. A computation is a function which takes an argument of type A and returns a “contextual” value of type M[A]:

def f[A](a: A): M[A]

So obviously we’re talking about parameterized types like Option[A] or Future[A]. These types provide some sort of context for the type they are parameterized with. An Option[A] expresses the fact that there might or might not be a value of type A. And the context a Future[A] defines is an asynchronous – i.e. delayed and potentially erroneous – computation of type A.

Here are two examples for a computation returning a future:

def f(n: Int): Future[Int] = Future(n + 1) // Not really rocket science ...
def g(n: Int): Future[Int] = Future(n - 1) // Can you guess what I have in mind?

Imagine we want to first call f with some value and then g with the value contained in f‘s result. Put another way: we want to sequence the dependent computations g and f by somehow pulling the value out of the context of the result of the first computation and passing it to the second.

Now we are facing the problem that a future might hold a value or not. This is, because its value is determined asynchronously and the future might not yet be completed. And if something goes wrong, a future will hold a failure instead of a value. Therefore simply grabbing into the future to pull out the value won’t work.

This is where monads shine, because when we say that some parameterized type M[A] is a monad that means that there is a function called flatMap which takes a value of M[A] and a function A => M[B] and returns a value of M[B]:

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

Getting back to our example, as a Future is a monad (well, monadic enough), it has a flatMap method. Therefore we can sequence f and g the following way:

val n = f(42).flatMap(g) // Same as f(42).flatMap(n => g(n))
assert(Await.result(n, 1.second) == 42)

So the monad itself – via its flatMap method – knows how to provide the value to the function to be sequenced. Hence instead of us trying to pull out its value we let the monad do this for us.

We could easily sequence more functions of the proper shape using flatMap, but that starts looking confusing quickly. Luckily there’s some special syntax support called for comprehensions which gets translated to flatMap calls:

def h(n: Int): Future[Int] = Future(n * 2)
 
val m = for {
  m1 <- f(42)
  m2 <- g(m1)
  m3 <- h(m2)
} yield m3
assert(Await.result(m, 1.second) == 84)

As you can see clearly, monads are for sequencing computations where the next computation depends on the value wrapped in the context of the previous one. That’s all that needs to be said, at least from a pragmatic perspective. Simple, eh?

If you want to dive deeper, here’s a totally incomplete suggestion of links:

Here comes the full example code (for the REPL):

import scala.concurrent.{ Await, Future }
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.DurationInt
 
def f(n: Int): Future[Int] = Future(n + 1)
def g(n: Int): Future[Int] = Future(n - 1)
def h(n: Int): Future[Int] = Future(n * 2)
 
val n = f(42).flatMap(g)
assert(Await.result(n, 1.second) == 42)
 
val m = for {
  m1 <- f(42)
  m2 <- g(m1)
  m3 <- h(m2)
} yield m3
assert(Await.result(m, 1.second) == 84)

Comment

Your email address will not be published. Required fields are marked *