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.

Consider this

Assume you implemented a function to combine many (e.g. five or more) optional values in an ordered sequence using flatMap. Can you think of any aspect of your implementation that could make your code difficult to maintain and read?

 

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.

  1. foo(Some(1))
  2. foo(Some(5))
  3. 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 returns true 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 of isDefined: it returns true if an optional instance is absent and false 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 with isDefined: it returns true if the value is present and it satisfies a given predicate, but returns false otherwise.
     
     def exists(predicate: A => Boolean): Boolean = ???
      
     Some(10).exists(_ > 5)     // returns true
     Some(1).exists(_ > 5)      // returns false
     None.exists(_ > 5)         // returns false
      
    

Think in Scala: don’t use the function get with Option

You may have noticed that Option has an implementation for a function called get: it returns the value if present, and it throws a java.util.NoSuchElementException if absent. Because it throws an exception, it’s considered unsafe to use and an anti-pattern because the compiler is no longer able to guarantee that your implementation won’t throw exceptions.

Don’t use the function get on Option. When tempted to do this, you should ask yourself if the optional type is an optional type. Maybe you should consider resolving the optional wrapper using pattern matching. Perhaps you need to re-evaluate the operations you’re performing on an optional value.

For example, consider the following function foo:

    def foo(a: Option[Int]): Int = if (a.isDefined) a.get else 0

It’s best if you rewrite it using the getOrElse operation on Option as follows:

    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:

  1. Retrieve the name of the tutor of a given student
  2. Find the id of the assistant of a professor tutoring a given student
  3. 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:

  1. The expression foo(Some(1)) evaluates to Some(10): all the optional values are present, and the chain of operations is completed, and its value returned.
  2. The function call foo(Some(5)) returns None: op(5) returns None, which causes the chain to break.
  3. foo(None) returns None because the first value of the for-comprehension expression is None.

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.