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 Option
s, 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 incase
classes. But once I (a) swore off usingnull
values, and (b) learned how to properly work withOption
s, I saw the beauty of the approach. For instance, one look at this class definition tells you that there may or may not be astreet2
field; as theOption
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 anOption
, so youflatMap
it to get aPerson
instance- Because
address
inPerson
is also anOption
, youflatMap
it to get anAddress
instance - Once you have the
address
, you yieldaddress.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 Option
s 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 Option
s. As shown, you can use (a) flatMap
or (b) a for
expression to traverse the Option
s 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.