|
From Get Programming with Scala by Daniela Sfregola The final part of the article series digs into using “for-comprehension” to chain optional values together. |
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:
- Chain optional values together using for-comprehension.
- Introduce conditions within a for-comprehension statement
- Code using the most common operations defined on
Option
You can use the functions map
, flatten
, and flatMap
to manipulate optional values. In this article, you’re going to discover a more readable and elegant way of combining instances of Option
thanks to a new type of statement, called “for-comprehension.” You’re also going to see how you can integrate Boolean conditions to further control how the values are chained together. I’ll also give you an overview of other useful operations implemented for Option
, such as isDefined
, getOrElse
, find,
and exists
. Make sure to check out part 1 and part 2 for more about Option.
For-comprehension on Option
The flatMap
function can be used to concatenate optional operation together. Let’s have a look at the code below:
def ownerDrivingLicense(optCar: Option[Car]): Option[String] = optCar.flatMap { car => car.owner.flatMap { person => person.drivingLicense } }
The code is fairly readable, but it requires you to nest many flatMap
function calls together. Consider that you must chain five or more optional operations together: your code should look something like the following:
opA().flatMap{ a => opB(a).flatMap { b => opC(b).flatMap { c => opD(c).flatMap { d => opE(d) } } } }
I like to refer to this as “rocket coding”: you write multiple nested operations that you’ll be forced to indent many times, making the code difficult to read. Scala has introduced some syntactic sugar—which is an alternative syntax to simplify verbose operations—for the map
and flatMap
functions: it’s called “for-comprehension.”
For-comprehension as syntactic sugar for nested map and flatMap calls
Listing 1 shows you how you can re-implement your ownerDrivingLicense
function using for-comprehension:
Listing 1: Example of for-comprehension
def ownerDrivingLicense(optCar: Option[Car]): Option[String] = for { car <- optCar person <- car.owner drivingLicense <- person.drivingLicense } yield drivingLicense
The expressions optCar
, car.owner
, and person.drivingLicense
all produce an optional value: each has type Option[Car]
, Option[Person]
, and Option[String]
respectively. The values car
, person
, and drivingLicense
are their corresponding extracted values: with types Car
, Person
, and String
. You’ve now encountered a new keyword, yield. It
returns a value that you can compute using the extracted values wrapped into an Option
. In this case, it returns the value of drivingLicense
as an optional value. As soon as the for-comprehension finds an absent optional value (e.g. car.owner
is None
), it evaluates the entire expression as None
.
You can use a for-comprehension statement on every class that has a flatMap
function. You’ll soon discover that this applies to types other than Option
. Thanks to it, your code can avoid an excessive nested structure, which makes it easier to read and understand. You can rewrite the earlier example of chained five or more operations as follows:
for { a <- opA() b <- opB(a) c <- opC(b) d <- opD(c) e <- opE(d) } yield e
QUICK CHECK 1 Consider the following snippet of code:
def foo(optA: Option[Int]) = for { a <- optA b <- f(a) c <- Some(5 * b) } yield c def f(n: Int): Option[Int] = if (n < 5) Some(n * 2) else None
What’s the value returned by each of the following function calls? Verify your hypothesis using the Scala REPL.
foo(Some(1))
foo(Some(5))
foo(None)
Filtering values within For-Comprehension
Assume that you want to modify your ownerDrivingLicence
function to return the drivingLicense
all uppercase and only for an owner with a given name. You could implement it using flatMap
as follows:
def ownerDrivingLicense(optCar: Option[Car], ownerName: String): Option[String] = optCar.flatMap { car => car.owner.flatMap { person => if (person.name == ownerName) person.drivingLicence.map(_.toUpperCase) else None } }
Listing 2 shows you how you can achieve the same using for-comprehension:
Listing 2: Example of for-comprehension with if condition
def ownerDrivingLicense(optCar: Option[Car], ownerName: String): Option[String] = for { car <- optCar person <- car.owner if person.name == ownerName //#A drivingLicense <- person.drivingLicense } yield drivingLicense.toUppercase //#B #A When the condition’s false, the chain stops the expression to return None. #B You can modify the yield value before returning it.
You can modify the yield values in a for-comprehension statement (e.g. drivingLicense.toUppercase
), as well as adding additional conditions to stop the combination of values by adding the keyword if
followed by a Boolean expression (e.g. if person.name == ownerName
). Have a look at figure 1 for a summary on for-comprehension statements in Scala.
Figure 1: Summary of for-comprehension on Option: they allow you to chain optional operations together thanks to their syntactic sugar to rewrite nested map and flatMap function calls.
QUICK CHECK 2 You have implemented a function ownerBelowAge
that returns the name of a car owner if younger than a given age: now re-implement it using for-comprehension.
def ownerBelowAge(car: Car, age: Int): Option[String] = ???
Other operations on Option
You know about the functions map
, flatten
and flatMap
for an optional value, but they aren’t the only ones. Other commonly used functions defined for an instance of Option[A]
are listed below:
isDefined
returnstrue
if an optional instance has a value, false otherwise.
def isDefined: Boolean = ??? Some(1).isDefined // returns true None.isDefined // returns false
- The function
isEmpty
is the opposite ofisDefined
: it returnstrue
if an optional instance is absent andfalse
otherwise.def isEmpty: Boolean = ??? Some(1).isEmpty // returns false None.isEmpty // returns true
getOrElse
returns the optional value if present, otherwise it executes the provided default operation.
def getOrElse(default: A): A = ??? Some(1).getOrElse(-1) // returns 1 None.getOrElse(-1) // returns -1
find
returns an optional value if its element satisfies a given predicate.
def find(predicate: A => Boolean): Option[A] = ??? Some(10).find(_ > 5) // returns Some(10) Some(1).find(_ > 5) // returns None None.find(_ > 5) // returns None
- The function
exists
combines the function find withisDefined
: it returnstrue
if the value is present and it satisfies a given predicate, but returnsfalse
otherwise.def exists(predicate: A => Boolean): Boolean = ??? Some(10).exists(_ > 5) // returns true Some(1).exists(_ > 5) // returns false None.exists(_ > 5) // returns false
def foo(a: Option[Int]): Int = if (a.isDefined) a.get else 0
def foo(a: Option[Int]): Int = a.getOrElse(0)
QUICK CHECK 3
Implement a function, called carWithLicensedOwner
, which takes and returns an optional car instance if its owner has a driving license:
def carWithLicensedOwner(optCar: Option[Car]): Option[Car] = ???
Summary
In this lesson, my objective was to teach you about the for-comprehension statement in Scala.
- You learned that a for-comprehension statement is an alternative, more readable way to rewrite a nested
flatMap
statement to chain optional values together. - You also saw how to add
if
statements that condition which values to consider in the statement. - You had a quick tour of other useful functions available on an instance of
Option
. - Let’s see if you got this!
Try this:
Let’s consider a scenario that describes a Student-Professor-Assistant relation:
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)
Implement the following functions using for-comprehension and the other operations on Option
you’ve seen in this article:
- 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 The answers are the follows:
- The expression
foo(Some(1))
evaluates toSome(10)
: all the optional values are present, and the chain of operations is completed, and its value returned. - The function call
foo(Some(5))
returnsNone: op(5)
returnsNone
, which causes the chain to break. foo(None)
returnsNone
because the first value of the for-comprehension expression isNone.
QUICK CHECK 2 You can re-implement the function ownerBelowAge
as follows:
def ownerBelowAge(car: Car, age: Int): Option[String] = for { person <- car.owner if person.age < age } yield person.name
QUICK CHECK 3 A possible implementation of the function carWithLicensedOwner
is the following:
def carWithLicensedOwner(optCar: Option[Car]): Option[Car] = optCar.find { car => car.owner.flatMap(_.drivingLicense).isDefined }
That’s all for now. If you’re interested in learning more about the book, check it out on liveBook here and see this slide deck.