The Neophyte's Guide to Scala Part 5: The Option Type

For the last couple of weeks, we have pressed ahead and covered a lot of ground concerning some rather advanced techniques, particularly ones related to pattern matching and extractors. Time to shift down a gear and look at one of the more fundamental idiosyncrasies of Scala: the Option type.

If you have participated in the Scala course at Coursera, you have already received a brief introduction to this type and seen it in use in the Map API. In this series, we have also used it when implementing our own extractors.

And yet, there is still a lot left to be explained about it. You may have wondered what all the fuss is about, what is so much better about options than other ways of dealing with absent values. You might also be at a loss how to actually work with the Option type in your own code. The goal of this part of the series is to do away with all these question marks and teach you all you really need to know about Option as an aspiring Scala novice.

The basic idea

If you have worked with Java at all in the past, it is very likely that you have come across a NullPointerException at some time (other languages will throw similarly named errors in such a case). Usually this happens because some method returns null when you were not expecting it and thus not dealing with that possibility in your client code. A value of null is often abused to represent an absent optional value.

Some languages treat null values in a special way or allow you to work safely with values that might be null. For instance, Groovy has the null-safe operator for accessing properties, so that foo?.bar?.baz will not throw an exception if either foo or its bar property is null, instead directly returning null. However, you are screwed if you forget to use this operator, and nothing forces you to do so.

Clojure basically treats its nil value like an empty thing, i.e. like an empty list if accessed like a list, or like an empty map if accessed like a map. This means that the nil value is bubbling up the call hierarchy. Very often this is okay, but sometimes this just leads to an exception much higher in the call hierchary, where some piece of code isn’t that nil-friendly after all.

Scala tries to solve the problem by getting rid of null values altogether and providing its own type for representing optional values, i.e. values that may be present or not: the Option[A] trait.

Option[A] is a container for an optional value of type A. If the value of type A is present, the Option[A] is an instance of Some[A], containing the present value of type A. If the value is absent, the Option[A] is the object None.

By stating that a value may or may not be present on the type level, you and any other developers who work with your code are forced by the compiler to deal with this possibility. There is no way you may accidentally rely on the presence of a value that is really optional.

Option is mandatory! Do not use null to denote that an optional value is absent.

Creating an option

Usually, you can simply create an Option[A] for a present value by directly instantiating the Some case class:

  1. val greeting: Option[String] = Some("Hello world")

Or, if you know that the value is absent, you simply assign or return the None object:

  1. val greeting: Option[String] = None

However, time and again you will need to interoperate with Java libraries or code in other JVM languages that happily make use of null to denote absent values. For this reason, the Option companion object provides a factory method that creates None if the given parameter is null, otherwise the parameter wrapped in a Some:

  1. val absentGreeting: Option[String] = Option(null) // absentGreeting will be None
  2. val presentGreeting: Option[String] = Option("Hello!") // presentGreeting will be Some("Hello!")

Working with optional values

This is all pretty neat, but how do you actually work with optional values? It’s time for an example. Let’s do something boring, so we can focus on the important stuff.

Imagine you are working for one of those hipsterrific startups, and one of the first things you need to implement is a repository of users. We need to be able to find a user by their unique id. Sometimes, requests come in with bogus ids. This calls for a return type of Option[User] for our finder method. A dummy implementation of our user repository might look like this:

  1. case class User(
  2. id: Int,
  3. firstName: String,
  4. lastName: String,
  5. age: Int,
  6. gender: Option[String])
  7. object UserRepository {
  8. private val users = Map(1 -> User(1, "John", "Doe", 32, Some("male")),
  9. 2 -> User(2, "Johanna", "Doe", 30, None))
  10. def findById(id: Int): Option[User] = users.get(id)
  11. def findAll = users.values
  12. }

Now, if you received an instance of Option[User] from the UserRepository and need to do something with it, how do you do that?

One way would be to check if a value is present by means of the isDefined method of your option, and, if that is the case, get that value via its get method:

  1. val user1 = UserRepository.findById(1)
  2. if (user1.isDefined) {
  3. println(user1.get.firstName)
  4. } // will print "John"

This is very similar to how the Optional type in the Guava library is used in Java. If you think this is clunky and expect something more elegant from Scala, you’re on the right track. More importantly, if you use get, you might forget about checking with isDefined before, leading to an exception at runtime, so you haven’t gained a lot over using null.

You should stay away from this way of accessing options whenever possible!

Providing a default value

Very often, you want to work with a fallback or default value in case an optional value is absent. This use case is covered pretty well by the getOrElse method defined on Option:

  1. val user = User(2, "Johanna", "Doe", 30, None)
  2. println("Gender: " + user.gender.getOrElse("not specified")) // will print "not specified"

Please note that the default value you can specify as a parameter to the getOrElse method is a by-name parameter, which means that it is only evaluated if the option on which you invoke getOrElse is indeed None. Hence, there is no need to worry if creating the default value is costly for some reason or another – this will only happen if the default value is actually required.

Pattern matching

Some is a case class, so it is perfectly possible to use it in a pattern, be it in a regular pattern matching expression or in some other place where patterns are allowed. Let’s rewrite the example above using pattern matching:

  1. val user = User(2, "Johanna", "Doe", 30, None)
  2. user.gender match {
  3. case Some(gender) => println("Gender: " + gender)
  4. case None => println("Gender: not specified")
  5. }

Or, if you want to remove the duplicated println statement and make use of the fact that you are working with a pattern matching expression:

  1. val user = User(2, "Johanna", "Doe", 30, None)
  2. val gender = user.gender match {
  3. case Some(gender) => gender
  4. case None => "not specified"
  5. }
  6. println("Gender: " + gender)

You will hopefully have noticed that pattern matching on an Option instance is rather verbose, which is also why it is usually not idiomatic to process options this way. So, even if you are all excited about pattern matching, try to use the alternatives when working with options.

There is one quite elegant way of using patterns with options, which you will learn about in the section on for comprehensions, below.

Options can be viewed as collections

So far you haven’t seen a lot of elegant or idiomatic ways of working with options. We are coming to that now.

I already mentioned that Option[A] is a container for a value of type A. More precisely, you may think of it as some kind of collection – some special snowflake of a collection that contains either zero elements or exactly one element of type A. This is a very powerful idea!

Even though on the type level, Option is not a collection type in Scala, options come with all the goodness you have come to appreciate about Scala collections like List, Set etc – and if you really need to, you can even transform an option into a List, for instance.

So what does this allow you to do?

Performing a side-effect if a value is present

If you need to perform some side-effect only if a specific optional value is present, the foreach method you know from Scala’s collections comes in handy:

  1. UserRepository.findById(2).foreach(user => println(user.firstName)) // prints "Johanna"

The function passed to foreach will be called exactly once, if the Option is a Some, or never, if it is None.

Mapping an option

The really good thing about options behaving like a collection is that you can work with them in a very functional way, and the way you do that is exactly the same as for lists, sets etc.

Just as you can map a List[A] to a List[B], you can map an Option[A] to an Option[B]. This means that if your instance of Option[A] is defined, i.e. it is Some[A], the result is Some[B], otherwise it is None.

If you compare Option to List, None is the equivalent of an empty list: when you map an empty List[A], you get an empty List[B], and when you map an Option[A] that is None, you get an Option[B] that is None.

Let’s get the age of an optional user:

  1. val age = UserRepository.findById(1).map(_.age) // age is Some(32)

flatMap and options

Let’s do the same for the gender:

  1. val gender = UserRepository.findById(1).map(_.gender) // gender is an Option[Option[String]]

The type of the resulting gender is Option[Option[String]]. Why is that?

Think of it like this: You have an Option container for a User, and inside that container you are mapping the User instance to an Option[String], since that is the type of the gender property on our User class.

These nested options are a nuisance? Why, no problem, like all collections, Option also provides a flatMap method. Just like you can flatMap a List[List[A]] to a List[B], you can do the same for an Option[Option[A]]:

  1. val gender1 = UserRepository.findById(1).flatMap(_.gender) // gender is Some("male")
  2. val gender2 = UserRepository.findById(2).flatMap(_.gender) // gender is None
  3. val gender3 = UserRepository.findById(3).flatMap(_.gender) // gender is None

The result type is now Option[String]. If the user is defined and its gender is defined, we get it as a flattened Some. If either the use or its gender is undefined, we get a None.

To understand how this works, let’s have a look at what happens when flat mapping a list of lists of strings, always keeping in mind that an Option is just a collection, too, like a List:

  1. val names: List[List[String]] =
  2. List(List("John", "Johanna", "Daniel"), List(), List("Doe", "Westheide"))
  3. names.map(_.map(_.toUpperCase))
  4. // results in List(List("JOHN", "JOHANNA", "DANIEL"), List(), List("DOE", "WESTHEIDE"))
  5. names.flatMap(_.map(_.toUpperCase))
  6. // results in List("JOHN", "JOHANNA", "DANIEL", "DOE", "WESTHEIDE")

If we use flatMap, the mapped elements of the inner lists are converted into a single flat list of strings. Obviously, nothing will remain of any empty inner lists.

To lead us back to the Option type, consider what happens if you map a list of options of strings:

  1. val names: List[Option[String]] = List(Some("Johanna"), None, Some("Daniel"))
  2. names.map(_.map(_.toUpperCase)) // List(Some("JOHANNA"), None, Some("DANIEL"))
  3. names.flatMap(xs => xs.map(_.toUpperCase)) // List("JOHANNA", "DANIEL")

If you just map over the list of options, the result type stays List[Option[String]]. Using flatMap, all elements of the inner collections are put into a flat list: The one element of any Some[String] in the original list is unwrapped and put into the result list, whereas any None value in the original list does not contain any element to be unwrapped. Hence, None values are effectively filtered out.

With this in mind, have a look again at what flatMap does on the Option type.

Filtering an option

You can filter an option just like you can filter a list. If the instance of Option[A] is defined, i.e. it is a Some[A], and the predicate passed to filter returns true for the wrapped value of type A, the Some instance is returned. If the Option instance is already None or the predicate returns false for the value inside the Some, the result is None:

  1. UserRepository.findById(1).filter(_.age > 30) // Some(user), because age is > 30
  2. UserRepository.findById(2).filter(_.age > 30) // None, because age is <= 30
  3. UserRepository.findById(3).filter(_.age > 30) // None, because user is already None

For comprehensions

Now that you know that an Option can be treated as a collection and provides map, flatMap, filter and other methods you know from collections, you will probably already suspect that options can be used in for comprehensions. Often, this is the most readable way of working with options, especially if you have to chain a lot of map, flatMap and filter invocations. If it’s just a single map, that may often be preferrable, as it is a little less verbose.

If we want to get the gender for a single user, we can apply the following for comprehension:

  1. for {
  2. user <- UserRepository.findById(1)
  3. gender <- user.gender
  4. } yield gender // results in Some("male")

As you may know from working with lists, this is equivalent to nested invocations of flatMap. If the UserRepository already returns None or the Gender is None, the result of the for comprehension is None. For the user in the example, a gender is defined, so it is returned in a Some.

If we wanted to retrieve the genders of all users that have specified it, we could iterate all users, and for each of them yield a gender, if it is defined:

  1. for {
  2. user <- UserRepository.findAll
  3. gender <- user.gender
  4. } yield gender

Since we are effectively flat mapping, the result type is List[String], and the resulting list is List("male"), because gender is only defined for the first user.

Usage in the left side of a generator

Maybe you remember from part three of this series that the left side of a generator in a for comprehension is a pattern. This means that you can also patterns involving options in for comprehensions.

We could rewrite the previous example as follows:

  1. for {
  2. User(_, _, _, _, Some(gender)) <- UserRepository.findAll
  3. } yield gender

Using a Some pattern in the left side of a generator has the effect of removing all elements from the result collection for which the respective value is None.

Chaining options

Options can also be chained, which is a little similar to chaining partial functions. To do this, you call orElse on an Option instance, and pass in another Option instance as a by-name parameter. If the former is None, orElse returns the option passed to it, otherwise it returns the one on which it was called.

A good use case for this is finding a resource, when you have several different locations to search for it and an order of preference. In our example, we prefer the resource to be found in the config dir, so we call orElse on it, passing in an alternative option:

  1. case class Resource(content: String)
  2. val resourceFromConfigDir: Option[Resource] = None
  3. val resourceFromClasspath: Option[Resource] = Some(Resource("I was found on the classpath"))
  4. val resource = resourceFromConfigDir orElse resourceFromClasspath

This is usually a good fit if you want to chain more than just two options – if you simply want to provide a default value in case a given option is absent, the getOrElse method may be a better idea.

Summary

In this article, I hope to have given you everything you need to know about the Option type in order to use it for your benefit, to understand other people’s Scala code and write more readable, functional code. The most important insight to take away from this post is that there is a very basic idea that is common to lists, sets, maps, options, and, as you will see in a future post, other data types, and that there is a uniform way of using these types, which is both elegant and very powerful.

In the following part of this series I am going to deal with idiomatic, functional error handling in Scala.

Posted by Daniel Westheide