Scala: Examples of for-expressions being converted to map and flatMap

Without much explanation today, here are a couple of source code examples from my book, Functional Programming, Simplified (in Scala). The only thing I’ll say about this code is that I created it in the process of writing that book, and the examples show how the Scala compiler translates for-expressions into map and flatMap calls behind the scenes.

1) How Options in for-expressions convert to map and flatMap

This for-expression:

val x = for {
    i <- Option(1)
    j <- Option(2)
    k <- Option(3)
} yield i + j + k

is the same as this use of flatMap and map:

val x: Option[Int] = Option.apply[Int](1).flatMap[Int] { (i: Int) =>
    Option.apply[Int](2).flatMap[Int] { (j: Int) => 
        Option.apply[Int](3).map[Int] { (k: Int) => 
            i.+(j).+(k)
        }
    }
}

which is essentially the same as this code:

val x: Option[Int] = Some(1).flatMap { (i: Int) =>
    Some(2).flatMap { (j: Int) => 
        Some(3).map { (k: Int) => 
            i + j + k
        }
    }
}

As a side note, if you haven’t worked with flatMap on Options yet, it can help to know that flatMap’s function should return an Option, like this:

Some(1).flatMap{ i => Option(i) }

2) How a Scala IO Monad converts (for-expression to map and flatMap)

Given these IO functions:

def getLine: IO[String] = IO(scala.io.StdIn.readLine());
def putStrLn(s: String): IO[Unit] = IO(println(s));

This for-expression that uses the IO Monad:

def doBlock: IO[Unit] = for {
    _         <- putStrLn("First name?")
    firstName <- getLine
    _         <- putStrLn(s"first: $firstName")
} yield Unit

doBlock.run

compiles to this map/flatMap code:

def doBlock: IO[Unit] = putStrLn("First name?").flatMap {_ => 
    getLine.flatMap { firstName => 
        putStrLn(StringContext("first: ", "").s(firstName)).map { _ => 
            Unit
        }
    }
};

doBlock.run

This is what that map/flatMap code looks like with its data types:

def doBlock: IO[Unit] = putStrLn("First name?").flatMap[Unit] { _: Unit => 
    getLine.flatMap[Unit] { firstName: String =>
        putStrLn(new StringContext("first: ", "").s(firstName)).map[Unit]{ _: Unit =>
            scala.Unit
        }
    }
}
  • notice that the flatMap’s have the return type Unit
  • that’s because the block has the return type Unit, or more accurately, the last flatMap/map call has that return type

Lessons learned

  • flatMap extracts the values out of monads
  • this code: _ <- putStrLn("First name?")
  • is equivalent to this code: putStrLn("First name?").flatMap {_ =>
  • this code: firstName <- getLine
  • is equivalent to this: getLine.flatMap[Unit] { firstName: String =>

3) Another IO Monad + for expression example

This code:

def getString: IO[String] = IO("yo ")    //returns the type `IO` (a monad)

def doBlock: IO[String] = for {
    a <- getString
    b <- getString
    c <- getString
} yield a + b + c

compiles to this code:

def getString: IO[String] = IO.apply[String]("yo ")

def doBlock: IO[String] = getString.flatMap[String] { a: String =>
    getString.flatMap[String] { b: String => 
        getString.map[String] { c: String => 
            a + b + c
        }
    }
}

Notes

- again, `flatMap` pulls the values out of the IO monad
      - getString yields `IO("yo ")`
      - getString.flatMap yields `a: String`
- to the right of each flatMap invocation is an anonymous function
- flatMap passes the unwrapped value into that anonymous function
- `a` and `b` are passed along until the final `getString` uses all of
  `a`, `b`, and `c`
      - `a` and `b` are essentially in the scope of the final `map` method’s
        anonymous function

4) Mapping a for-expression to map and flatMap

One more example of how a for-expression translates to map and flatMap calls:

def doBlock: IO[String] = for {
    a <- getString     //getString.flatMap[String] { a: String => ...
    b <- getString     //[a]   => getString.flatMap[String] { b: String => ...
    c <- getString     //[a,b] => getString.map[String] { c: String => a + b + c }
} yield a + b + c
  • the map and flatMap invocations each yield a String
  • Strings are passed down the chain as a, b, and c
  • Strings are passed back up the chain
  • a <- getString MAPS TO getString.flatMap { a: String => ... - the a comes out of getString, and it's made available to the next line
  • b <- getString MAPS TO getString.flatMap { b: String => ... - this anonymous function has access to [a,b]