Know Thy Option

A thing or two about Option

  • Avoid .get at all costs. Forget there is even a .get function on Option. There is always an alternative to .get. Same applies to .head
  • If you are going to have access the value in an Option in a test class1, prefer extending your test class from OptionValues. Then you can use .value on an Option, which fails with a relatively better error message if the value is not defined.
  • Option maybe viewed as a sequence (of zero or one element). This is for convenience when working with Option, which is why see a .head on an Option.

The Different Options

Following are the different ways to avoid the use of .get or .head to yield a value from an Option.

map and flatMap

When you think of reaching into an Option for its value, map or flatMap is the defacto choice and is safe because they allow us to safely reach into Option only if there is a value inside. They may be chained back to back to attempt a series of transformations on the Option. Since the resultant type of the transformations is still an Option, it is common for map or flatMap to end with one of the getOrElse , fold or is pattern matched to resolve to a value.

val maybeGreeting: Option[String] = ...

def personalizeGreeting(g: String): String = ...

val mayBeGreetingBanner = maybeGreeting.map(personalizeGreeting)

// ---------------------------------------------------------------

val maybeGreetingKey: Option[String] = getGreetingKeyConfigName()

def readGreetingValue(g: String): Option[String] = ...

val maybeGreeting = maybeGreetingKey.flatMap(k => readGreetingValue(k))

getOrElse

val maybeGreeting: Option[String] = ...

val g: String = maybeGreeting.getOrElse("Hello!")

val g: String = maybeGreeting.map(_.upperCase).getOrElse("HELLO!")

getOrElse provides a default or replacement value if the Option does not have a value. It is commonly used in cases where the intent is to resolve to a value by optionally running a sequence of transformations; a common default value if none of the transformations in the pipeline yields a value.

val maybeGreeting: Option[String] = maybeValueFromConfig()

val simpleStyleGreeting: String =
   maybeGreeting.getOrElse("Hello!")

val yelling: String = 
  maybeGreeting
    .map(_.upperCase)
    .getOrElse("HELLO!")

val greetingAfterTransformations =
  mayBeGreeting
    .flatMap(maybeValueFromSource1)
    .flatMap(maybeValueFromSource2)
    .flatMap(maybeValueFromSource3)
    .getOrElse("Hello!")

greetingAfterTransformations may have one of the following values:

  1. maybeValueFromConfig
  2. Otherwise, Hello! even if one of the transformations (flatMap) does not yield non-empty Option
  3. Otherwise, the string after running each of the transformations – maybeValueFromSource1, maybeValueFromSource2, maybeValueFromSource3.

The subtlety in greetingAfterTransformations is that it is not explicit which transformation did not yield a value and was defaulted with Hello!.

orElse

Consider orElse as the dual of flatMap. While flatMap runs when the Option has a value, orElse does the opposite. It runs when the Option does not have a value. Like flatMap, orElse expects an Option back from the evaluated expression.

import cats.syntax.option._
import cats.instances.functor._

val maybeG: Optional[String] =
  maybeGreeting.orElse("Hello!".some)

Pattern match

One of the facilities that would have

val g: X =
  maybeGreeting match {
    case Some(g) => ...
    case None => ...
  }

where X is the type of value returned by the match expression.

import cats.syntax.option._
import cats.instances.functor._

val maybeGreeting: Option[String] = ...

val g: String = maybeGreeting.getOrElse("Hello!")

val g: String = maybeGreeting.map(_.upperCase).getOrElse("Hello!")

val maybeG: Optional[String] = maybeGreeting.orElse("Hello!".some)

val g: String =
  maybeGreeting match {
    case Some(g) => ...
    case None => ...
  }

val g: String =
  maybeGreeting.fold("Hello!") { g =>
  if (g.startsWith("How")) s"$g?"
  else s"$g!"
  }

val maybeG: Option[String] =
  maybeGreeting.innerMap {
    case Some(g) if g.startsWith("GG") => ...
    case Some("How are you?") => ...
  }

fold

When you want to resolve to a value with explicit paths for the empty and

val g: String =
  maybeGreeting.fold("Hello!") { g =>
    if (g.startsWith("How")) s"$g?"
    else s"$g!"
  }

So …

As you can see, there is a myriad of options to avoid .get or .head, each with a different style and purpose and fitting different situations. However, why should we avoid .get or .head?_

You might have heard this before many times. Allow me to say it. .get/ .head, let us call them unsafe getters, makesit hard to reason about code. In other words, you cannot positively say that the unsafe getters will or will not throw exception. All the above ways to avoid the unsafe getters exactly help with that. If you are in a certain code path, you can absolutely be certain that the Option has or does not have a value.

In the same vein, the argument applies to Either too.



  1. Test code is real time code. It is used / executed several dozen times a day. Its quality is equally vital to a robust application. So, it is natural to develop a lot of classes and facilities to write better quality tests. It is recommended to reserve the use of OptionalValues.value in the tests themselves rather than in the facilities supporting the tests. 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.