- Avoid
.get
at all costs. Forget there is even a.get
function onOption
. There is always a way out - better one, than using.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 fromOptionValues
. Then you can use.value
on anOption
. Doing so establishes the presence of value as verification with meaningful error if value is not defined.
Did you know ?
Option
maybe viewed as a sequence (of zero or one element). This is for convenience when working withOption
, which is why see a.head
on anOption
.
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 match
ed 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:
-
maybeValueFromConfig
-
Otherwise,
Hello!
even if one of the transformations (flatMap
) does not yield non-emptyOption
-
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 withHello!
.
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!"
}
Finally
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. You did not ask the question: why should we avoid .get
or .head
?
-
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. ↩︎