|
From Get Programming with Scala by Daniela Sfregola This article introduces the |
Take 37% off Get Programming with Scala. Just enter fccsfregola into the discount code box at checkout at manning.com.
After reading this article, you’ll be able to:
- Represent a nullable value using
Option
- Use pattern matching on instances of the type
Option
After mastering high order functions, you’re going to learn about the type Option
. In Scala, using null
to represent nullable or missing values is an anti-pattern: use the type Option
instead. The type Option
ensures that you deal with both the presence and the absence of an element. Thanks to the Option
type, you can make your system safer by avoiding nasty NullPointerException
s at runtime. Your code will also be cleaner as you won’t need to preventively check for null values: you’ll be able to clearly mark nullable values and act accordingly only when effectively needed. The concept of an optional type isn’t exclusive to Scala: if you’re familiar with another language’s Option type, such as Java, you’ll recognize a few similarities between them. You’re going to learn about the structure of the Option
type. In part 2, you’re going to explore map
and flatmap
. In part 3, you’re going to explore changing Option
values with for-comprehension
.
Why Option?
Suppose you’ve defined the following function to calculate the square root of an integer:
def sqrt(n: Int): Double = if (n >= 0) Math.sqrt(n) else null
This function has a fundamental problem. Its signature—what a function does—doesn’t provide any information about its return value being nullable: you’ll need to look at its implementation—this is how a function computes a value—and remember to deal with a potentially null
value. This approach is particularly prone to errors as you can easily forget to handle the null case, causing a NullPointerException
at runtime, and it forces you to write a lot of defensive code to protect your code against null
:
val x: Int = ??? val result = sqrt(x) if (result == null) { //protect from null here } else { // do things here }
The type Option
is equivalent to a wrapper around your value to provide the essential information that it may be missing. Thanks to the use of Option,
you no longer need to look at the specific implementation of a function to discover if its return value is nullable: this information is in its signature. The compiler also makes sure that you handle both the cases when it’s present and when it’s absent, making your application safer at runtime.
Creating an Option
After discussing how the use of Option
can improve the quality of your code, let’s see how you can create instances for it. A nullable value either exists or it’s missing: the (simplified) definition of Scala’s Option
shown in listing 1 reflects this structure.
Listing 1: The Option
Type
package scala sealed abstract class Option[A] case class Some[A](a: A) extends Option[A] case object None extends Option[Nothing]
Let’s analyze its definition line by line and see what each of them mean:
package scala
The
Option
type lives in thescala
package, and it’s already available into your scope without the need of an explicit import.sealed abstract class Option[A]
Option
is an abstract class, and you can’t initialize it directly. It’s sealed: it has a well-defined set of possible implementations (i.e.,Some
of a given value andNone
). For the first time, you encountered the Scala notation for “generics,” which isOption[A]
. An optional type works independently from the value it contains: you would expect an optional value to work in the same way of an optional integer, an optional string or any other optional value. With the annotationOption[A]
, you’re telling the compiler that you’ll associateOption
with a type that you’ll provide as a parameter in initialization:Option
has a “type parameter”. Scala has a convention of using upper case letters of the alphabet for type parameters, the reason whyOption
usesA
, but you could provide any other name for it.case class Some[A](a: A) extends Option[A]
Some
is case class and a valid implementation ofOption
that represents the presence of a value. It has a type parameterA
that defines the type of the value it contains.Some[A]
is an implementation ofOption[A]
, which implies thatSome[Int]
is a valid implementation forOption[Int]
, but not forOption[String]:
scala> val optInt: Option[Int] = Some(1) optInt: Option[Int] = Some(1) scala> val optString: Option[String] = Some(1) <console>:11: error: type mismatch; found : Int(1) required: String val optString: Option[String] = Some(1)
Scala’s type inference is about to infer type parameters. For this reason, you can write
Some(1)
instead ofSome[Int](1).
case object None extends Option[Nothing]
None is the other possible implementation for
Option,
and it represents the absence of a value. It’s a case object, which means it’s a serializable singleton object. The idea of a missing value is applicable to a value independently from its type: for this reason,None
doesn’t have a type parameter, but it extendsOption[Nothing]
.Nothing
has a special meaning in Scala:Nothing
is the subclass of every other class; it’s the opposite ofAny
(see figure 1):
Figure 1: In Scala, the types
Any
andNothing
have a special meaning.Any
is the superclass of every other class—it’s the root of the class hierarchy.Nothing
is at the bottom of the class hierarchy—it’s the subclass of every other class.
Because None
extends Option[Nothing]
, you can use None
as a valid implementation for Option
independently of its type parameter. Thanks to its special meaning, Nothing
will always be compatible with the type parameter you provided:
scala> val optInt: Option[Int] = None optInt: Option[Int] = None scala> val optString: Option[String] = None optString: Option[String] = None
The terms None
, Nothing
and null
can get confusing here; let’s recap what each of them means. None
is an instance of the class Option,
and it represents a missing nullable value. Nothing
is a type that you can associate with all other Scala types. The term null
is a keyword of the language to indicate a missing reference to an object.
scala> import scala.reflect.runtime.universe._ import scala.reflect.runtime.universe._ scala> classOf[String] res0: Class[String] = class java.lang.String scala> typeOf[String] res1: reflect.runtime.universe.Type = String
scala> classOf[Option[Int]] res2: Class[Option[Int]] = class scala.Option scala> classOf[Option[Int]] == classOf[Option[String]] res3: Boolean = true scala> typeOf[Option[Int]] res4: reflect.runtime.universe.Type = scala.Option[Int] scala> typeOf[Option[String]] res5: reflect.runtime.universe.Type = scala.Option[String] scala> typeOf[Option[Int]] == typeOf[Option[String]] res6: Boolean = false
You can now re-implement your sqrt
function to use the type Option
as shown in listing 2:
Listing 2: The sqrt
function with Option
def sqrt(n: Int): Option[Double] = if (n >= 0) Some(Math.sqrt(n)) else None
Figure 2 provides a visual summary of Scala’s Option
type.
Figure 2: Visual summary of the structure of the Scala’s Option
type. An Option[A]
is either a Some[A]
of a value a
or None
, which is the representation for its absence.
Quick check 1
Write a function called filter
which takes two parameters of type String
, called text
and word
, and it returns an Option[String]
. The function should return the original text if it contains the given word, otherwise it should return no value.
val log: PartialFunction[Int, Double] = { case x if x > 0 => Math.log(x) }
def log(x: Int): Option[Double] = x match { case x if x > 0 => Some(Math.log(x)) case _ => None }
Pattern Matching on Option
After looking at the structure of an Option
, let’s see how you can handle it.
When pattern matching on a sealed item, the compiler warns you if you haven’t considered all its possible implementations, as this could cause a MatchError
exception. You can pattern match on case classes and case objects. Let’s put everything together and see how you can pattern match and get an optional value.
When handling an optional value, one possibility is to use pattern matching to consider both the presence and the absence of a value:
Listing 3: Pattern Matching over Option
def sqrt(n: Int): Option[Double] = if (n >= 0) Some(Math.sqrt(n)) else None def sqrtOrZero(n: Int): Double = sqrt(n) match { ❶ case Some(result) => result ❷ case None => 0 ❸ }
❶ sqrt(n)
returns a value with type Option[Double]
❷ if the value is present, return it
❸ if the value is missing, return 0
Note that pattern matching isn’t the only way to handle an optional value: in part 2, I’ll show you how you can achieve the same using predefined high order functions such as map
and flatMap
.
Summary
In this article my objective was to teach you about Scala’s Option
type:
- You discovered how you can use it to represent nullable values and how this can improve the quality of your code.
- You learned how to create instances of
Option
and see how to handle them using pattern matching.
Let’s see if you got this!
Answers to Quick Checks
def filter(text: String, word: String): Option[String] = if (text.contains(word)) Some(text) else None
def greetings(customMessage: Option[String]): String = customMessage match { case Some(message) => message case None => "Greetings, Human!" }
That’s all for now. Stay tuned for part 2. If you’re interested in learning more about the book, check it out on liveBook here and see this slide deck.