Scala: Handling nested Options with flatMap and for

Summary: In this article I show two ways to extract information from nested optional fields in your Scala domain models. This example is a little contrived, but if you have a situation where one Option instance contains one or more other Options, this article may be helpful.

Optional fields in your domain model

There are times when you’re creating your domain model when it makes sense to use optional fields in your case classes. For instance, when you model an Address, the “second street address” isn’t needed for all people, so making it an optional field makes sense:

case class Address(
    street1: String,
    street2: Option[String],
    city: String,
    state: String,
    zip: String
)

True confession: When I first started working with Scala I used to hate using Option fields in case classes. But once I (a) swore off using null values, and (b) learned how to properly work with Options, I saw the beauty of the approach. For instance, one look at this class definition tells you that there may or may not be a street2 field; as the Option wrapper type implies, its optional.

Taking this a step further, it’s also possible that when you create a new Person instance in an application, you might not know the person’s address, so that field may also be optional:

case class Person(
    name : String, 
    address : Option[Address]
)

So now Address has the optional field street2, and Person has the optional field address.

When Options contain other Options (nested Options)

Now imagine a case where you have an Option[Person] in an application. For instance, maybe you’re getting a list of all of the salespeople for a region, and because some positions may not be filled, that query returns a list of Option[Person], i.e., a List[Option[Person]]. Next, further assume that you’re working on a piece of code that needs to get the street2 portion of each person’s address. How would you do this?

When you start writing this function, you know that you’ll be given an Option[Person] as input, so you can begin to sketch the function signature like this:

def getStreet2(maybePerson: Option[Person]) ...

Next, you know that street2 is optional — an Option[String] — so you can add the function’s return type:

def getStreet2(maybePerson: Option[Person]): Option[String] = ...

Now all you have to do is to fill out the body of the function to match that signature.

Using flatMap

One way to handle Options that contains other Options is to use flatMap. This solution shows how to flatMap each Option to get the result you want:

def getStreet2(maybePerson: Option[Person]): Option[String] = {
    maybePerson flatMap { person =>
        person.address flatMap { address =>
           address.street2
        }
    }
}

While most people prefer for expressions — more on that shortly — this is a common use of flatMap. Depending on your needs, you can use a similar approach with any monad, i.e., Try, Either, List, Future, etc.

Here’s a quick explanation of the getStreet2 code:

  • maybePerson is an Option, so you flatMap it to get a Person instance
  • Because address in Person is also an Option, you flatMap it to get an Address instance
  • Once you have the address, you yield address.street2

As I wrote in my book, Functional Programming, Simplified, when you write code like this, all you have to do is focus on the “happy path,” i.e., the success case. In this algorithm, the success case means that all of those Options will return Some instances, and you’ll get the street2 field in the end. As I also mention in the book, the “unhappy path” takes care of itself.

Note: There is a cleaner version of this approach in the Comments section below.

Some sample data

A couple of examples will show how getStreet2 works. First, in this example, I use None for the street2 field, so getStreet2 yields a None:

scala> val a1 = Address("123 Main Street", None, "Talkeetna", "Alaska", "99676")
a1: Address = Address(123 Main Street,None,Talkeetna,Alaska,99676)

scala> val p = Person("Al", Some(a1))
p: Person = Person(Al,Some(Address(123 Main Street,None,Talkeetna,Alaska,99676)))

scala> getStreet2(Some(p))
res0: Option[String] = None

Second, in this example I do have a street2 address (“Apt. 1”), so getStreet2 yields a Some:

scala> val a2 = Address("123 Main Street", Some("Apt. 1"), "Talkeetna", "Alaska", "99676")
a2: Address = Address(123 Main Street,Some(Apt. 1),Talkeetna,Alaska,99676)

scala> val p = Person("Al", Some(a2))
p: Person = Person(Al,Some(Address(123 Main Street,Some(Apt. 1),Talkeetna,Alaska,99676)))

scala> getStreet2(Some(p))
res0: Option[String] = Some(Apt. 1)

This shows that getStreet2 works as desired.

But if you don’t like flatMap ...

Most humans like for expressions

While my brain is at a point where the flatMap code now makes sense and is readable, most people prefer for expressions to flatMap, so I suspect that they’d write getStreet2 like this:

def getStreet2(maybePerson: Option[Person]): Option[String] = {
    for {
        person  <- maybePerson
        address <- person.address
        street2 <- address.street2
    } yield street2
}

If you take the time to use my sample data with that function in the REPL, you’ll see that it works just like the flatMap version of getStreet2.

Motivations

My main reason for writing this article was to demonstrate two ways to deal with Option instances that contain other Options. As shown, you can use (a) flatMap or (b) a for expression to traverse the Options to get the value you want.

A second reason for writing this article was to give you a little more exposure to flatMap. When I first learned Scala, I always thought of flatMap in terms of collections classes, but once you get into other monadic data types like Option, Try, Either, etc., you realize that there’s a very different way to think about flatMap, as shown in this example.

For those who have read Functional Programming, Simplified, you might know that I have a cheeky grin on my face when I use the term, “monadic,” so here’s a ;) for you.

See also

This article was inspired by an article titled, FP for the average Joe - II - ScalaZ Monad Transformers.