Scala: a recap of map, flatMap and Options

A (not so) quick intro/recap to map, flatMap and Options in Scala.

Map & For Comprehension

Map() evaluates a function over each element in the list, returning a list with the same number of elements.

Example: do nothing

val numbers = List(1,2,3,4,5)

numbers map ((n: Int) => n)

numbers map (n => n)

A for comprehension is a pretty way of saying the same: iterate through a list, and apply some function to every item.

for (n <- numbers) yield n

Example: double every odd element

numbers filter ((p: Int) => p % 2 == 1) map ((n : Int) => n * 2)

numbers filter (p => p % 2 == 1) map (n => n * 2)

numbers filter (_ % 2 == 1) map (_ * 2)

The for comprehension equivalent is

for (n <- numbers if n % 2 == 1) yield n * 2

which btw is almost the same as Python’s list comprehensions:

[ n * 2 for n in range(1,6) if n % 2 == 1 ]

flatMap

The signatures for map and flatMap are:

def map[B](f: (A) => B): Traversable[B]

def flatMap[B](f: (A) => GenTraversableOnce[B]): Traversable[B]

the signature for flatMap tells us how it works, namely by applying a function that

  1. returns a sequence for each element in the list
  2. flattens the result into the original list

in fact if I try to feed to flatMap a function that does not return a GenTraversableOnce it won’t compile, since you can’t possibly flatten a flat list!

List(1,2,3) flatMap (x => x)
// fails: expected Int => GenTraversableOnce[B],
//        actual Int => Int 

scala> List(1,2,3) flatMap (x => List(x))

scala> List(1,2,3) flatMap (x => Some(x))
// these are fine

Example: using flatMap to flatten a nested list

val nested = List(List(1,2), List(3,4))

nested flatMap((x: List[Int]) => x: List[Int])
// List(1,2,3,4)

This takes a list of int, returns a list of int, and flattens the result. The same can also be expressed by manually “unrolling” the nested list, and flattening the result:


nested map ((x: List[Int]) => x map ((n: Int) => n)) flatten

nested map (x => x map (n => n)) flatten

To be fair, this last example is a bit weird because I could actually just flatten the nested list without “unrolling” it first; if I did the unrolling is because a) it makes for a good example, forcing me to reason about the inner structure of the object I’m dealing with; and b) it actually becomes necessary if I want to do something with the inner objects, e.g.

nested flatMap (inner => inner map (value => value * 2))

Syntactic sugar for map and flatMap

Chaining multiple map and flatMap calls can become hard to read, so I can spread it over multiple lines. I can write the following

nested flatMap (innerList => innerList map (value => value * 2))

using the curly brackets syntax for map/flatMap

nested flatMap { innerList =>
  innerList map { value =>
    value * 2
  }
}

or using a for comprehension

for {
  list <- nested
  n <- list
} yield n * 2

Dealing with Options

Check the nice tutorial linked at the bottom of this post for a throughout explanation of Options. The TL;DR version is that Options can be seen as a kind of list that contains either zero elements or one; if I think of it as a list it makes sense to use functional combinators on it!

Say that I have a method like

def findById(id: Int): Option[User]

said method will return either Some(user) or None, and I want to avoid null pointer exceptions when I try to do something with the user but I get None instead.

There are three ways to deal with this scenario:

1) I can manually check if the user is defined

val user = UserRepository.findById(1)
if (user.isDefined) {
  println(user.get.firstName)
} 

which is not that nice.

2) I can use pattern matching:

UserRepository.findById(1) match {
  case Some(user) => println(user.name)
  case None => println("lookup failed")
}

This is useful especially when I want to be explicit about what to do if the lookup fails, e.g. I want to log a warning message and exit the app.

3) I can use functional combinators

UserRepository.findById(2).foreach(user => println(user.name))

UserRepository.findById(2).map(_.age) // Some(33)

The latter method is especially neat because I can chain multiple calls to functional combinators which will only be executed if some value is present. On the other hand, it means that the code will always succeed, and it’s going to be harder to handle the case when the lookup fails.

Mapping over Options

To conclude, here’s some mock classes to play with mostly as a boring exercise:

case class Country(name: String, capital: Option[City])
case class City(name: String, hasRiver: Option[Boolean])

val london = City("London", Some(true))
val milan = City("Milan", None)
val england = Country("England", Some(london))
val italy = Country("Italy", None)

Since london.hasRiver is a list, the signatures for map and flatMap are

map[B] (f: (Boolean => B)) // returns Option[B]
flatMap[B] (f: (Boolean => Option[B])) // returns Option[B]

the signature for map is telling me that what map does here is:

  1. it takes a Boolean (remember I’m mapping through Option[Boolean])
  2. it converts it to whatever type B
  3. it returns an Option of whatever type is B, because map preserves the shape of the monad I applied it to

following the same logic, flatMap returns Option[B] – and not Option[Option[B]] – because it flattens the list, so that:

london.hasRiver map(x => x)
london.hasRiver flatMap (x => Some(x))

// they both returns Some(true)

Since capital is an Option, I have to treat it as if it were a list:

england.capital.hasRiver // doesn't compile
england.capital.get.hasRiver // works

The problem with this is, if capital is None, I get a NullPointerException – and rightly so. Thus, is good practice to always pass Options to a functor, that will return None instead of blowing in my face:

england.capital.foreach(city => println(city.name)) // London
italy.capital.foreach(city => println(city.name)) // nothing
 
italy.capital.get.hasRiver // blows up bc no capital
italy.capital.flatMap(city => city.hasRiver) // None
italy.capital.flatMap(_.hasRiver) // same but prettier

What flatMap here is saying is pretty much “go through the list that contains either a city or nothing, and if you find any, return city.hasRiver“. As if it was a nested list, hasRiver, being a nested Option, would return an Option[Option[Boolean]] if called with a map instead of a flatMap.

Anyhow, you really don’t want to call get on a None – remember hasRiver is an Option, so you’re still getting either Some(boolean) or None – so is key to use getOrElse() instead:

italy.capital.flatMap(_.hasRiver).getOrElse("nope")

For anything more complicated than two levels of nested options (it happens all the time, for example when you model a Json response to a case class) the for comprehension is easier to read, even though you can achieve the same result chaining maps and flatMaps.

For instance, if we add Continents to the model:

case class Continent(name: String, countries: Option[List[Country]])

val europe = Continent("Europe", Some(List(england, italy)))

the following are equivalent:

for {
  countries <- europe.countries
  capital <- countries.head.capital
  river <- capital.hasRiver
} yield river // Some(true)

and

europe.countries.flatMap { countries =>
  countries.head.capital.flatMap { capital =>
    capital.hasRiver
  }
} // Some(true)

if we unroll the for comprehension:

  1. countries is an Option[List[Country]]
  2. countries.head is an Option[Country] of the first country in the list, i.e. Some(Country(England,Some(City(London,Some(true))
  3. capital is an Option[City], i.e. Some(City(London,Some(true)))
  4. river is an Option[Boolean], i.e. Some(true)

References