developer blog

Back to Originate.com

Idiomatic Scala: Your Options Do Not Match

Idiomatic Scala: Your Options Do Not Match

Imagine you are reviewing a teammate’s work when you are confronted with the following piece of code. What would you think?

1
2
3
4
5
if (expr == true) {
  return true
} else {
  return false
}

Cast the first semicolon who never saw (or maybe even wrote) similar code. While it works and is strictly speaking correct, the code above disappoints for expressing what it does a little too literally. It is unsophisticated and ingenuous, a neophyte code smell almost. Experienced developers probably prefer the more compact idiom:

1
return expr

Yet, it is very common to see even seasoned Scala developers decomposing Option via pattern matching:

1
2
3
4
def f[A](opt: Option[A]) = opt match {
  case Some(a) => ???
  case None => ???
}

Pattern matching on Option is almost never necessary as usually there are cleaner and more expressive ways to attain the same results with monadic functions. This is what the Option Scaladoc has to say:

The most idiomatic way to use an scala.Option instance is to treat it as a collection or monad and use map, flatMap, filter, or foreach […] A less-idiomatic way to use scala.Option values is via pattern matching

Any expression of the form

1
2
3
4
opt match {
  case Some(a) => foo
  case None => bar
}

can be written more concisely and precisely equivalent as

1
opt map foo getOrElse bar

Furthermore, since Scala 2.10 an even more compact alternative is available, fold:

1
opt.fold(bar)(foo)

fold even gives us the additional benefit of being more type-strict than either of the previous alternatives. For instance, while a and b in the example below have a type of Any (with value 2), c fails to compile:

1
2
3
4
5
6
7
8
9
10
val opt = Option(1)

val a = opt match {
  case Some(x) => x + 1
  case None => "a"
}

val b = opt map (_ + 1) getOrElse "a"

val c = opt.fold("a")(_ + 1)

On the not-so-bright side, fold may not be as readable as the alternatives and a little error-prone, too, since it is not clear what comes first, None or Some?

Readable or not, we still can do a lot better than fold: Option offers a few dozen methods, many of which can be used to better express more specific transformations. For example, opt.fold(true)(_ => false) is equivalent to opt.isEmpty. The converse is either opt.isDefined or its alias opt.nonEmpty.

The Ultimate Scala Option Cheat Sheet

We reproduce below a table highlighting the most relevant Option methods. Whenever you feel like writing case Some(a) => foo; case None => bar consult this table for the equivalent foo and bar in the Some(a) and None columns to find its corresponding method. It is also recommended that you refer to the Option Scaladoc for detailed descriptions and additional methods.

First, a few definitions. For arbitrary types A and B, let:

1
2
3
4
val a: A
def p(a: A): Boolean // a predicate
def f[B](a: A): B // a mapping function
def g[B](a: A): Option[B] // an optional mapping function
Method Meaning Some(a) None
map(f) Applies f to its content Some(f(a)) None
getOrElse(default) Retrieves its content or a default value a default
fold(default)(f) (2.10) Same as map f getOrElse default f(a) default
isEmpty Is it a None? false true
isDefined Is it a Some? true false
nonEmpty (2.10) Same as isDefined true false
size Same as isDefined, but returning 0 or 1 1 0
flatMap(g) Same as map, except g returns Option g(a) None
orElse(Option(default)) Same as getOrElse, but inside an Option Some(a) Option(default)
orNull For legacy code that expects null a null
filter(p) Turns a Some into None if its content does not conform to p Some(a) if p(a), otherwise None None
find(p) Same as filter Some(a) if p(a), otherwise None None
filterNot(p) (2.9) Same as filter(!p) Some(a) if !p(a), otherwise None None
contains(b) (2.11) Whether its content is equal to b a == b false
exists(p) Does its content conform to p? p(a) false
forall(p) (2.10) Same as exists, except None returns true (see note below) p(a) true
count(p) Same as exists, but returning 0 or 1 1 if p(a), otherwise 0 0
foreach(f) Applies side-effectful f to its content Unit (calls f(a)) Unit
flatten (2.10) “Unnest” a nested Option. Compile error if a itself is not an Option a None
toRight(default) Converts to Either Right(a) Left(default)
toLeft(default) Converts to Either Left(a) Right(default)
toList Converts to List List(a) Nil
toSeq Same as toList List(a) Nil
toSet Converts to Set Set(a) Set()
get Do not bother about what this is supposed to do, never use it :–) a NoSuchElementException

.exists and .forall

The most attentive readers may be baffled by why None.forall returns true.

If you have your favorite predicate logic introductory book at hand, you may want to read the sections on “universal quantification” and “vacuous truth”. If not, trust us, the math checks out :–)

Here is a colloquial explanation. Basically, it boils down to forall being the reciprocal negative (this may not be the correct mathematical term) of exists. This is easier to understand with an example:

1
2
3
4
5
def isPrime(n: Int): Boolean = ???

val a = Seq(2, 3, 5, 7)

val b = Seq(1, 2, 4, 8)

We can clearly see that a.forall(isPrime) is true, while a.forall(!isPrime) is false. However, for list b, both b.forall(isPrime) and b.forall(!isPrime) are false. Negating the predicate does not necessarily negates the result. The negation of forall(isPrime) is not forall(!isPrime), but exists(!isPrime). In other words:

1
forall(isPrime) == !exists(!isPrime)

It makes perfect sense in plain English: to say “all numbers are prime” is equivalent to saying “there does not exist a number that is not prime”.

Back to Option, None.exists(p) is always false, as is None.exists(!p). Therefore, by the identity derived above:

1
2
3
None.forall(p) == !None.exists(!p)
               == !false
               == true

As unintuitive and surprising as this result may be, it is true and correct, proven by much more rigorous mathematical derivation. As a matter of fact, this apparent oddity can be put to some pretty good use, as we will see next.

A more elaborate example

Let us start with a simple method: tracking events on a webpage. The only distinctive characteristic here is that whether we have a user logged in to the site, we record their ID.

1
2
3
4
def track(user: Option[User]) = user match {
  case Some(u) => Tracker.track(u.id)
  case None => Tracker.track(GUEST)
}

Now, due to privacy concerns, we have to allow users to opt out of tracking. Easy-peasy:

1
2
3
4
def track(user: Option[User]) = user match {
  case Some(u) if u.canTrack => Tracker.track(u.id)
  case _ => Tracker.track(GUEST)
}

All was going well, until one day we got a call from our lawyers: if a user opts out, they should never be tracked at all, not even anonymously. Hence, our final version:

1
2
3
4
5
def track(user: Option[User]) = user match {
  case Some(u) if u.canTrack => Tracker.track(u.id)
  case None => Tracker.track(GUEST)
  case _ => // do not track
}

Now let us convert everything to not use pattern matching. The first one is trivial:

1
Tracker.track(user map (_.id) getOrElse GUEST)

Not only did we replace the match with a more compact single-line expression, we do not have to write Tracker.track twice anymore. It may seem inconsequential, but over time such small things tend to grow in complexity, making maintenance harder.

The second version is more interesting. Whenever we see pattern guards, filter may be required. Overall, it does not change much from above:

1
Tracker.track(user filter (_.canTrack) map (_.id) getOrElse GUEST)

Finally, something more interesting. Now we have not two, but three case clauses. Clearly, there is no single method in Option that handles three possible outcomes. The answer, however, is surprisingly simple once you know it:

1
2
if (user forall (_.canTrack))
  Tracker.track(user map (_.id) getOrElse GUEST)

Knowing that None.forall is always true was crucial to allow us to come up with a simple expression that handles all cases.

Tip: Prefer Option(a) over Some(a)

You may have noticed in some examples that we wrote val a = Option(1) instead of val b = Some(1). Usually, one should prefer Option(x) over Some(x). First, a is inferred as type Option[Int], while b is Some[Int]. Secondly, and more importantly, is that Option(x) is more type-safe than Some(x). Again, let us see why with an example:

1
2
3
4
5
6
// We are enthusiastic about newcomers
def hello(name: Option[String]) = s"Hello, ${name getOrElse "stranger" toUpperCase}!"

def a(name: String) = hello(Option(name))

def b(name: String) = hello(Some(name))

Which is better? Method a or method b? Call a(null) and b(null) and see the answer for yourself.

Trivia

All Option methods in the Scala standard library are implemented as single line methods that are slight variations of the form:

1
if (isEmpty) bar else foo

Most methods are also marked as @inline final, which means that using Option higher-order functions should be as efficient as safe-guarding for null in Java, especially after HotSpot does its magic. How cool is that?

Addendum: Do not abuse pattern matching

Pattern matching is a very expressive and powerful tool. However, as frequently is the case with the finest hammers, we must watch out for the proverbial nail.

Pattern match all the things

Pattern matching “all the things” must be considered a Scala code smell. Especially for some common cases, pattern matching is not always as readable or concise as other constructs, and often the bytecode generated by the Scala compiler is less efficient, too.

In particular, do not pattern match on Booleans; use good old, true and tested if else:

1
2
3
4
5
6
cond match {
  case true => Ok
  case false => Error
}

if (cond) Ok else Error

Avoid pattern matching on a single value followed by a defaut case:

1
2
3
4
5
6
7
8
n match {
  case 0 => "Cart is empty"
  case _ => "View cart"
}

if (n > 0) "View cart" else "Cart is empty"

s"Your cart has ${if (n > 0) n else "no"} item(s)."

Finally, for most data structures like Seq and String, consider methods like isEmpty, headOption, tail, etc. And for enumerations, use withName.

Enjoy your Options!

Comments