From Get Programming with Scala by Daniela Sfregola

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 using map
  • 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.

 

Consider this

What are the fundamental operations that you can perform on an optional value? What if you have more optional values that need to be combined?

 

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 of Option
  • 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 function f 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 an Option.
  • You discovered that you can use the flatten function to unify nested optional structures.
  • You learned that the flatMap operation is the combination of a map followed by a flatten 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:

  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

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.