Almost every programming languages have certain mechanism to deal with the null
values, i.e. the absence of some values, in order to work safely in the messy real world. Many languages choose to have a separate object for these absent values, e.g. the null
in Java. And defensive programming teaches us to handle this scenario separately, given whether it is a value that is returned or it is the null
object. So, you might find the this description familiar: the null
checkers are placed all over the codebase and specific logics are needed to handle each one of them.
But Scala thinks different. It handles the null
values in a very elegent way via the Option
class. Read on if you are tired of those null
checkers like I am.
Some basics
In short, if you have a value of type A
that may be absent, Scala uses an instance of Option[A]
as its container. An Intance of Option
is either an instance of case class Some
when it is present or case object None
when it is not. Since both Some
and None
are children of Option
, your function signature should declare that the returned value is an Option
of some type, e.g. Option[A]
in the above example. In that way, the users of that function are forced by the complier to handle the possibility of absence, which follows the Principle of Least Astonishment.
It is very easy to create an Option
in Scala, i.e. you can use a present/absent value directly.
1 | val optionalInt: Option[Int] = Some(1) |
Or, if you know a function may not return a value, you could specify its return value as Option
.
1 | def foo: Option[String] = { ... } |
Similarly, you can write Option[A]
anywhere you think a value of type A
may be absent.
1 | def foo(param: Option[A]): ... |
Working with Option
Hopefully you are sold by now that the concept of Option
is pretty intuitive and neat. In that case, you are probably already eager to know how to actually work with it. Well, just like almost everything in Scala, there are many ways to work with Option
. For better or worse, Scala is optimized for flexibility and easy writability. Now let me walk you through some of the more common approaches.
isDefined
This approach should be quite familiar to you. It is very similar to what you would do for languages with a separate null
object. That if, you add a checker for the None
value using the isDefined
method and specify logic to handle each scenario accordingly.
Suppose we need a function addTwo
that takes an integer value, which might be absent (treats the input as zero if not present), and adds it by two, the function below would meet the requirements. Note that, if the input is present, i.e. a.isDefined
, we use the get
method to get its value.
1 | def addTwoWithDefault(a: Option[Int]): Int = { |
pattern matching
Another similar approach is to use pattern matching, given Some
is a case class. I can rewrite the addTwo
function in the following way with pattern matching.
1 | def addTwoWithDefault(a: Option[Int]): Int = { |
getOrElse
In many cases, you have a fallback or default value for your absent values, e.g. zero in the above example. With Option
, you can easily provide a default value via the getOrElse
method.
1 | def addTwoWithDefault(a: Option[Int]): Int = a.getOrElse(0) + 2 |
One thing to keep in mind is that getOrElse
doesn’t preserve the type of the Option
and you should be cautious to specify the type of the default value.
1 | getOrElse[B >: A](default: ⇒ B): B |
The above approaches may not look better than dealing with null
in Java. And, you might have guessed it already, these use cases are not really where Scala Option
shines. Now, let’s look at some of the more idiomatic way to process Option
in Scala.
flatten
For similicity, assume that we have a List
of Option[Int]
.
1 | val l: List[Option[Int]] = List(Some(3), Some(1), None, Some(5), Some(8), None) |
A common scenario is that we need to filter out the absent values and return a List
of Int
. A straightfoward approach is to combine filter
with .isDefined
.
1 | l.filter(_.isDefined).map(_.get) |
However, Scala actually provides an elegent built-in function to achieve the same goal, which is often more preferred.
1 | l.flatten |
flatMap
Along the same line, suppose we want to add two to each of the present value in the List
and calculate their summation, we can use flatMap
to get there easily.
1 | def addTwo(a: Option[Int]): Option[Int] = { |
Ending
I personally find Option
to be a very intuitive way to deal with null
values. The code tends to be more readable and informative to function callers, because they are expected to handle it explicitly. As a bonus point, Option
even comes with all the functional goodness about Scala collections.
Hope you will start to handle unexpected absent values in a more “expected” way using Option
and get rid of those ugly null
checkers lying around the codebase.