A grave misunderstanding

When I started using Scala, I was very confused by “for comprehensions” for a very long time. My mistake was to treat <- as an “unwrapper”. For example, in the following, <- “unwraps” the collection/container/monad, as expected.

  def main(args: Array[String]): Unit = {
    for { x <- Option(1) } { println(x) } // prints 1
    for { x <- List(2) } { println(x) } // prints 2
    for { x <- Try(3) } { println(x) } // prints 3

We also know that for comprehension is sugar for flatMap, map and withFilter1. So we can combine enumerators like the following to produce very succinct and elegant code.

    val _: List[Int] = for {
      x <- List(1)
      y <- List(x + 1)
    } yield y
    // Is sugar for 
    val z: List[Int] = List(1).flatMap(x => List(x + 1).map(y => y))

So far so good. Sometimes, we want to do some computation over the enumerators:

    def isOdd(x: Int): Option[Int] = if ((x % 2) == 0) None else Some(x)
    val z: List[Int] = for {
      x <- List(1)
      y <- isOdd(x)
    } yield y

This makes sense at first if we treat <- as unwrapping the Option[Int]. However, that is a wrong mental modal. If we try to replace Option with Try, we see that the code fails to compile.

    def isOdd(x: Int): Try[Int] = Try(x + 1) // Note that this is now Try instead of Option
    val z: List[Int] = for {
      x <- List(1)
      y <- isOdd(x) // This will fail to compile
    } yield y

This took me by surprize for an embarassingly long time. I was confused as to why it was fine to use for comprehension for List and Option, but not List and Try. Similarly Try and Option Fails too2.

Recall that the type signature of flatMap is (or equivalently) F[A] => (A => F[B]) => F[B] and that F stays the same. So it makes sense that combining Try and Option wouldn’t work. However, why is it that List and Option works? It turns out that the implementation of flatMap for List takes a GenTraversableOnce and an there is an implicit conversion from Option to an Iterator which satisfy that trait.

I think this is an example of some of the things people moving to Scala will be caught up by and probably should be emphasized by books introducing the language.

  1. https://docs.scala-lang.org/tour/for-comprehensions.html ↩︎

  2. If a linguist were to look and tried to derive the syntactic rule, she’d be confused too. Let’s replace List, Option, and Try with the symbols A, B, C, and let combining them with “for comprehension” be the binary operator +. Then the observed rules are: A + A, B + B, C + C, A + B are fine, but A + C, B + C does not work. It is unclear why things break for C. ↩︎