|
This part of the article series delves into using map to transform an object contained in an Option and how to chain optional values together using flatMap. |
Take 37% off Get Programming with Scala. Just enter fccsfregola into the discount code box at checkout at manning.com.
After reading this article, you’ll be able to:
- Transform an element contained in an
Option
usingmap
- Simplify a nested optional structure using
flatten
- Chain optional values together using
flatMap
You may have discovered the type Option,
and how to pattern match with it. After working with optional types for some time, you’ll realize that some of its operations are particularly recurrent: Option
offers you a set of high order functions for them, allowing you to be more productive and not need to use pattern matching every time. In this article, I’ll introduce you to some of the most common and useful predefined functions on Option
. You’re going to learn how to transform an optional value using map.
You’ll see how to simplify a nested optional structure using flatten.
Finally, you are going to learn how to combine optional values in an order sequence using flatMap
. These functions describe patterns which are common to many Scala types other than Option
: understanding them’s crucial because you’re going to encounter them in many different contexts. In part 3, you’re going to explore changing Option
values with for-comprehension
. For a quick intro to Option
, check out part 1.
Transforming an Option
Let’s start by showing you how you can transform the content of an optional value using map, flatten,
and flatMap
, which are one of the most fundamental and recurrent helper functions defined on Option
.
Suppose you need to represent the following scenario involving a car and its owner as follows:
- A car has a model, and it may have an owner and a registration plate.
- A person has a name and age, and may have a driving license.
- A car may have no owner (e.g., when it’s brand new).
- A car may have no registration plate if its owner hasn’t registered the vehicle with the local authorities yet.
- A person without a driving license is still entitled to purchase a car.
You can translate the above requirements using two case classes, called Car
and Person
:
Listing 1: The Car
and Person
case classes>
case class Car(model: String, owner: Option[Person], registrationPlate: Option[String]) case class Person(name: String, age: Int, drivingLicense: Option[String])
Let’s see how the functions map
, flatten
, and flatMap
can help you extracting information on your data set.
The map function
Suppose you would like to find the name of the owner of a particular car. You could use pattern matching as follows:
def ownerName(car: Car): Option[String] = car.owner match { case Some(p) => Some(p.name) case None => None }
You can rewrite this function using the function map
rather than pattern matching as follows:
Listing 2: Example usage of map
on Option
def ownerName(car: Car): Option[String] = car.owner.map(p => p.name)
In Scala, you refer to the operation of applying a function to the content of an optional value as map
. The function map
is a high order function defined on Option
that takes a function f
as its parameter:
- If the optional value is present, it applies the function
f
to it and return it wrapped as optional value; - If the optional instance is
None
, it returns it without applying any function.
A possible implementation of map
for a given Option[A]
is shown in listing 3:
Listing 3: The function map
on Option
def map[B](f: A => B): Option[B] = this match { //#A case Some(a) => Some(f(a)) case None => None }
#A “this” is a keyword that refers to the current instance of the class.
Compare the implementation of map
with your implementation of the function ownerName
using pattern matching: they’re similar and have the same structure! Let’s have another look at the signature of the map
function for the abstract class Option[A]
:
def map[B](f: A => B): Option[B] = ???
You don’t need to remember what a map
function does, and you need to look at its signature: if you have an instance of Option[A]
and you have a function f
that transforms an A into a B, then you know how to obtain a value of type Option[B]
through to the map
function. Figure 1 provides a summary of how the high order function map
operates on an instance of Option
.
Figure 1: Visual representation of the map
operation on Option
. If the optional value is present, map
applies the function f
to it and wraps the result in a Some
instance. If the optional value is absent, it returns None
without applying any function f
.
quick check 1
Consider the case class Car
shown in listing 1. Write a function, called extractRegistrationPlate
, which takes an instance of Car,
and it returns an optional registration plate with its text all upper case: use the function map
on Option
.
def extractRegistrationPlate(car: Car): Option[String] = ???
The flatten function
Suppose you need to retrieve the driving license of an owner of a car, if any. You could achieve this using map as follow:
def ownerDrivingLicense(car: Car): Option[Option[String]] = car.owner.map(_.drivingLicense)
The ownerDrivingLicense
returns a value of type Option[Option[String]]
: the first optional type indicates if an owner exists, the second if the owner has a driving license. In a particular business context, you may want to keep this distinction, but it doesn’t seem natural in a general case. As human we expect the value either to be there or not. We don’t say that a value “may, may be there,” but simply that a value “may be there.”
You can use the flatten
function to combine two nested optional values as follows:
Listing 4: Example usage of flatten
on Option
def ownerDrivingLicense(car: Car): Option[String] = car.owner.map(_.drivingLicense).flatten
The function flatten
acts on an instance of Option[Option[A]],
and it returns an Option[A]:
- If the outer optional value is a
Some
of a value, return the inner instance ofOption
- If the other optional value is
None
, return it
A possible implementation of the function flatten
on an instance of Option[Option[A]]
is the following:
Listing 5: The function flatten
on Option
def flatten: Option[A] = this match { case Some(opt) => opt case None => None }
The function flatten
merges two optional values into one: if you need to flatten more than two values, you can re-apply it multiple times.
quick check 2
Write a function, called superFlatten
, that takes an instance of Option[Option[Option[String]]]
, and it returns a value of type Option[String]
using the function flatten
.
def superFlatten(opt: Option[Option[Option[String]]]): Option[String] = ???
The flatMap function
In listing 5, you’ve implemented a function to extract the driving license of a car owner using the function map
together with the flatten
function:
def ownerDrivingLicense(car: Car): Option[String] = car.owner.map(_.drivingLicense).flatten
Two operations combined are common enough that Scala has created an ad-hoc function for it, called flatMap
. Listing 6 shows you how you can rewrite the ownerDrivingLicense
function using the flatMap
function rather than combining map
and flatten
operations.
Listing 6: Example usage of flatMap
on Option
def ownerDrivingLicense(car: Car): Option[String] = car.owner.flatMap(_.drivingLicense)
In Scala, the function flatMap
is a high order function on Option[A]
that applies a function f
, which returns an optional value itself – in other words, f
has type A => Option[B]
:
- If the optional value is present, it applies the function
f
to it. The functionf
returns an optional value, and you don’t need to wrap the result in an optional value. - If the optional instance is
None
, it returns it without applying any function.
A possible implementation of flatMap
using pattern matching is the following:
Listing 7: The function flatMap
on Option
def flatMap[B](f: A => Option[B]): Option[B] = this match { case Some(a) => f(a) case None => None }
Have a look at figure 2 for a visualization representation of the flatMap
function on Option
.
Figure 2: Visual representation of the flatMap
operation on Option
. If the value is present, flatMap
applies the function f
to it, which produces a new optional value. If the optional value is absent, it returns None
without applying any function f
.
quick check 3
Write a function, called ownerBelowAge, which takes two parameters: an instance of Car
and an age parameter of type Int
. It returns an optional string containing the name of the car owner if younger that then the age parameter. Use the flatMap
function on Option
:
def ownerBelowAge(car: Car, age: Int): Option[String] = ???
Let’s have another look at the flatMap
function to understand why it’s powerful. Consider you have an instance of an optional car and you would like to extract the driver’s license of its owner, if any. Its implementation using pattern matching could be similar to the following:
def ownerDrivingLicense(optCar: Option[Car]): Option[String] = optCar match { case None => None case Some(car) => car.owner match { case None => None case Some(person) => person.drivingLicense } }
Having two nested pattern matching statements makes your code extremely difficult to read. You could get rid of the nested pattern matching by taking advantage of the fact that both Car and Person are case classes, and you can easily decompose them when pattern matching:
def ownerDrivingLicense(optCar: Option[Car]): Option[String] = optCar match { case Some(Car(_, Some(Person(_, _, drivingLicense)), _)) => drivingLicense case None => None }
This second version of your code is a bit more readable, but strictly coupled to the structure of your case classes: you need to remember the order of their fields to implement the function correctly, and modify it every time you add, remove or reorder them. Finally, let’s try to use the flatMap
function:
Listing 8: Chaining optional values with flatMap
def ownerDrivingLicense(optCar: Option[Car]): Option[String] = optCar.flatMap { car => car.owner.flatMap { person => person.drivingLicense } }
The code is more readable and independent from the specific structure of the involved case classes. In the next section, I’ll show you a fourth and even more readable way of reimplementing this same function using “for-comprehension”.
Because of its unique structure, the flatMap
function allows you to chain multiple optional operations together. Figure 3 provides a visual summary of how you can flatMap
to combine multiple optional values in an ordered sequence. Consider you have an instance of Option[A]
and due functions f: A => Option[B]
and g: B => Option[C]
: by calling flatMap(f)
you can transform your instance into an Option[B]
, and then apply flatMap(g)
to it and obtain an instance of Option[C]
.
Figure 3: Example of chaining optional operations together using flatMap
. Apply flatMap(f)
to an instance of Option[A]
to produce an instance of Option[B]
. Then, apply flatMap(g)
to the instance of Option[B]
and obtain an instance of Option[C].
Summary
In this article, my objective was to teach you about transforming an optional value.
- You’ve seen that the
map
function applies a function to the element contained in anOption
. - You discovered that you can use the
flatten
function to unify nested optional structures. - You learned that the
flatMap
operation is the combination of amap
followed by aflatten
function and it’s used to combine optional values in an ordered sequence.
Let’s see if you got this!
Try this:
Consider the following scenario:
- A student has an id and a name.
- A student may have a professor assigned as a tutor.
- A professor has an id and a name.
- A professor may have the help of an assistant.
- An assistant has an id and a name.
You can translate the above to code with the following case classes:
case class Student(id: Long, name: String, tutor: Option[Professor]) case class Professor(id: Long, name: String, assistant: Option[Assistant]) case class Assistant(id: Long, name: String)
Write functions to extract the following information:
- Retrieve the name of the tutor of a given student
- Find the id of the assistant of a professor tutoring a given student
- Return a given student only if they have a tutor with a given id
Answers to Quick Checks
quick check 1
A possible implementation for the function extractRegistrationPlate
is the following:
def extractRegistrationPlate(car: Car): Option[String] = car.registrationPlate.map(_.toUpperCase)
quick check 2
You can implement the superFlatten
function by applying the flatten
function twice as follow:
def superFlatten(opt: Option[Option[Option[String]]]): Option[String] = opt.flatten.flatten
Notice how much simpler this implementation is compared to its equivalent using pattern matching.
quick check 3
A possible implementation for the function ownerBelowAge
is the following:
def ownerBelowAge(car: Car, age: Int): Option[String] = car.owner.flatMap { p => if (p.age < age) Some(p.name) else None }
That’s all for this part, stay on the lookout for part 3. If you’re interested in learning more about the book, check it out on liveBook here and see this slide deck.