1. Overview

In this tutorial, we’re going to learn how to manage optional data elements in Scala. Scala’s Option type makes it easier to write robust code if we use it as intended throughout our application code.

2. Option Basics

Managing and representing optional values requires some careful thought. As we have all experienced, writing code that handles the absence of data is difficult to write and is the source of many runtime errors.

Scala’s Option is particularly useful because it enables management of optional values in two self-reinforcing ways:

  1. Type safety – We can parameterize our optional values.
  2. Functionally aware – The Option type also provides us with a set of powerful functional capabilities that aid in creating fewer bugs.

Moreover, learning and mastering the Option class is a great way to adopt Scala’s functional paradigms in our code. We’ll talk about those along the way.

Let’s start with how Scala represents optional values:

           Option[T]
              ^
              |
      +-------+------+
      |              |
      |              |
    Some[T]        None[T]

The base class, scala.Option, is abstract and extends scala.collection.IterableOnce. This makes Scala’s Option behave like a container. This is a capability we can put to good use when we discuss mapping and filtering with Options later in this tutorial.

The important takeaway here is that the Option class and its subclasses all require a concrete type, which can be either explicit or inferred:

val o1: Option[Int] = None
val o2 = Some(10)

Both o1 and o2 are instances of Option[Int].

Finally, it’s useful to know that Option is “null aware”:

val o1: Option[Int] = Option(null)
assert(false == o1.isDefined)

This is handy when interoperating with Java, especially older Java libraries that use null to represent “not found”.

2.1. Test an Option for Some or None

We can test whether an Option is Some or None using these following methods:

  • isDefinedtrue if the object is Some
  • nonEmptytrue if the object is Some
  • isEmptytrue if the object is None

2.2. Retrieving an Option‘s Contents

We can retrieve the Option‘s value via the get method. If we invoke the get method on an instance of None, then a NoSuchElementException will be thrown. This behavior is referred to as “success bias”. Becoming comfortable with success bias is an important step in our Scala journey, and the Option type is a great place to start that journey.

Because of this, it’s tempting to write code like:

val o1: Option[Int] = ...
val v1 = if (o1.isDefined) {
  o1.get
} else {
  0
}

Or, its pattern matching counterpart:

val o1: Option[Int] = ...
val v1 = o1 match {
  case Some(n) =>
    n
  case None =>
    0
}

Both of these are considered anti-patterns in the Scala community because of their reliance on non-functional coding constructs. Other Option anti-patterns include using if/else or match to map an Option to another Option. There are more idiomatic and elegant ways to achieve the same effect using existing methods that we’ll explore later in this article.

2.3. Option Default Values

We also have a couple of other retrieval methods:

  • getOrElse – Retrieve the value if the object is Some, otherwise return a default value
  • orElse – Retrieve the Option if it is Some, otherwise return an alternate Option

The first method, getOrElse, is useful in situations where we want to return a default value if the optional value is unset. Let’s rewrite the snippet of code in the previous section to use getOrElse:

val v1 = o1.getOrElse(0)

Clearly, this is simpler and easier to maintain. Additionally, we’re not limited to simply returning a value. This is also equally valid:

val usdVsZarFxRate: Option[BigDecimal] = ... // populated via a web service call, for example
val marketRate = usdVsZarFxRate.getOrElse(throw new RuntimeException("No exchange rate defined for USD/ZAR"))

Here, imagine that we have a Federal Exchange rate that we retrieved via a web service call. But perhaps users have to be enabled to get those rates. In the case that they have not been enabled, the web service code will return None. This gives a quick way to exit any such code block since we cannot do any of the subsequent work.

3. Options as a Container

As we have discussed, an Option is a container for another value. In this regard, it can be treated as a special case of a collection, albeit with a single value. Therefore, we can traverse an Option in the same way as we traverse a List. Combined with Scala’s functional programming support, this enables us to write code that is more succinct and with fewer errors.

3.1. Mapping Options

The formal definition of the Option.map method is:

final def map[B](f: (A) => B): Option[B]

Let’s unpack this by way of some examples:

val o1: Option[Int] = Some(10)
assert(o1.map(_.toString).contains("10"))
assert(o1.map(_ * 2.0).contains(20))

val o2: Option[Int] = None
assert(o2.map(_.toString).isEmpty)

It should come as no surprise that we can use Option.map to convert the contained value to another type.

Additionally, Option provides the flatMap method to be used to collapse multiple layers within the Option.

Let’s explore these concepts using a simple set of traits and classes for tracking a games tournament:

trait Player {
  def name: String
  def getFavoriteTeam: Option[String]
}

trait Tournament {
  def getTopScore(team: String): Option[Int] // Specified team's top score or None if they haven't played yet
}

val player: Player = ...                     // Let's not worry how we instantiate these just yet
val tournament: Tournament = ...

How do we get a player’s favorite team’s top score? We could call player.getFavoriteTeam().map(tournament.getTopScore) but this returns an Option[Option[Int]]. Working with multiple layers of Option is awkward, to be sure.

Instead of using Option.map, we can use another method, Option.flatMap. It does exactly what we want by removing intermediate layers of Options. Let’s update our implementation of getTopScore to use it:

def getTopScore(player: Player, tournament: Tournament): Option[(Player, Int)] = {
  player.getFavoriteTeam.flatMap(tournament.getTopScore).map(score => (player, score))
}

Finally, if we just want to access the value of the Option, we can use the foreach method. This is useful when we don’t care about using the value. It is often used in “side-effecting” situations, like if we want to store the value in a database or print it to a file:

val o1: Option[Int] = Some(10)
o1.foreach(println)

This will simply print the value of o1 to stdout.

3.2. Flow Control with Options

We can also use Option.map to effect flow control. Consider the following:

val o1: Option[Int] = Some(10)
val o2: Option[Int] = None

def times2(n: Int): Int = n * 2

assert(o1.map(times2).contains(20))
assert(o2.map(times2).isEmpty)

Here we have defined two instances of an Option[Int]. The first with a value of 10 and the second, empty. We then define a simple function that simply multiplies its input by 2.

Finally, we map each of the Option‘s, o1, and o2 using the times2 function. Unsurprisingly, mapping o1 gives us Some(20) and mapping o2 gives us None.

The important takeaway here is that in the second call, o2.map(times2), the times2 function is not called at all.

Let’s consider a more complex example using some other methods provided by Option that allow us to implement flow control.

3.3. Flow Control with Filtering

We can also do collection-style filtering on Options:

  • filter – Returns the Option if the Option‘s value is Some and the supplied function returns true
  • filterNot – Returns the Option if the Option‘s value is Some and the supplied function returns false
  • exists – Returns true if the Option is set and the supplied function returns true
  • forall – Same behavior as exists since Options have a maximum of one value

These methods allow us to write some very concise code. For example, let’s consider a situation where we need to calculate which of two users’ favorite teams has the top score:

 def whoHasTopScoringTeam(playerA: Player, playerB: Player, tournament: Tournament): Option[(Player, Int)] = {
    getTopScore(playerA, tournament).foldRight(getTopScore(playerB, tournament)) {
      case (playerAInfo, playerBInfo) => playerBInfo.filter {
        case (_, scoreB) => scoreB > playerAInfo._2
      }.orElse(Some(playerAInfo))
    }
  }

Here, we leverage the fact that Options can behave like collections. The difference here is that an Option can have at most one value.

Code like this may seem opaque at first, but as we become more comfortable with Scala idioms, especially the power of its monadic operators, this will become easier for us to use. Note that we never have to explicitly check for None, nor do we have to do explicit if/else checks.

4. Conclusion

In this article, we looked at the basic use of Scala’s Option class. We expanded on that basic knowledge to show how we can use Option‘s functional interface to write concise and idiomatic Scala.

As always, the full source code can be found over on GitHub.

guest
0 Comments
Inline Feedbacks
View all comments