Nesting monads in Scala

Recently I write a lot of async code. Most of my repository methods return types like Future[Set[T]] or Future[Option[T]]. But as we will see, working with such types in pure Scala can be very cumbersome.

For example in pure Scala we cannot write:

val namesFuture = Future.successful(List("bob", "alice"));

val capitalizedNames = for {
  names <- namesFuture
  name <- names
} yield name.capitalize

Nop. Nada. Will not work. When we try to compile this code, the compiler will point out that names have type of List[String] instead of expected Future[X].

To understand the problem better lets remind ourselves how Scala compiler translates for-comprehensions into method calls:

val ks = for {
  i <- 1 to 10
  j <- 1 to i
  k <- 1 to j
  sum = i + j + k
  if (sum > 10 && sum < 20)
} yield 3*sum
// Is translated (with some simplifications) into:
val ks2 = (1 to 10).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j)
      .map { k => i + j + k }
      .withFilter { sum => sum > 10 && sum < 20 }
      .map { sum => 3*sum }
  }
}

In short every but the last “assignment” of the form var <- something is translated into something.flatMap { var => .... The last “assignment” is translated into a simple map call. if filters are translated into withFilter or filter calls.

Returning to our first example we see that it is translated into:

val capitalizedNames = for {
  names <- namesFuture
  name <- names
} yield name.capitalize
// into this:
val capitalizedNames = namesFuture.flatMap { names =>
  names.map(_.capitalize)
}

And indeed it does not type check as namesFuture.flatMap expects that the passed lambda will return a Future[X] not a List[X].

We can quickly fix this problem by introducing a nested for or by replacing flatMap by map:

val capitalizedNames = for { names <- namesFuture } yield
                       for { name <- names } yield name.capitalize;
// or:
val capitalizedNames = namesFuture.map { names =>
  names.map(_.capitalize)
}
// of if you are processing only a single collection:
val capitalizedNames = for { names <- namesFuture }
                       yield names.map(_.capitalize)

And even in this simple example, the method chain becomes quite unreadable when you try to squash it into a single line: namesFuture.map(_.map(_.capitalize)).

Exactly the same problems appears when we try to work with Future[Option[T]]. But here we can at least use libraries to reduce the pain. For example using OptionT type from Cats, we can write:

import cats.data.OptionT
import cats.implicits._

val nameFuture = Future.successful(Option("foo"))

val f = OptionT(nameFuture)
  .map(name => name + "!")
  .map(name => println(s"name is $name"))
Await.result(f.value, Duration.Inf)

…and call it a day.

In pure Scala this code would look like this:

val f = nameFuture
  .map(_.map(name => name + "!"))
  .map(_.foreach(n => println(s"name is $n")))
Await.result(f, Duration.Inf)

In short I don’t understand why language designers decided to not support nested monads in for-comprehensions. It’s a pity that we have to use external libraries to get such a basic functionality.