ARTICLE
Using Option in Scala, Part 1: introduction
From Get Programming with Scala by Daniela Sfregola
__________________________________________________________________
Take 37% off Get Programming with Scala. Just enter fccsfregola into the discount code box at checkout at manning.com.
__________________________________________________________________
This article introduces the Option
type and discuss why you would (or wouldn't) want to use it and when.
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 learn how to create optional values and how to handle them using pattern matching. In part 3, you're going to explore changing Option
values with for-comprehension
.
Consider this
Suppose you need to design a structure to represent nullable values: how would you indicate that an element in it may or may not be there?
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.
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 the scala
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 and None
). For the first time, you encountered the Scala notation for "generics," which is Option[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 annotation Option[A]
, you're telling the compiler that you'll associate Option
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 why Option
uses A
, 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 of Option
that represents the presence of a value. It has a type parameter A
that defines the type of the value it contains. Some[A]
is an implementation of Option[A]
, which implies that Some[Int]
is a valid implementation for Option[Int]
, but not for Option[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 of Some[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 extends Option[Nothing]
. Nothing
has a special meaning in Scala: Nothing
is the subclass of every other class; it's the opposite of Any
(see figure 1):
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.
Think in Scala: Class versus Type
In this article, I’ve used the terms class and type as synonymous for simplicity, but they are not the same in all cases.
A class represents a code element that has a particular behavior and that you can instantiate through its constructor. A type uniquely identifies a much broader category of items you can use in your programs. Let’s use the Scala REPL to see a few examples in action.
String has same class and type:
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 = StringThis isn’t the case when comparing Option[Int] with Option[String]: they both belong to the class Option but they have different types: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:
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.
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.
Convert Partial Functions to Total Functions returning Option
Partial functions can be a tool to abstract commonalities between functions. An example of a partial function is the following:
val log: PartialFunction[Int, Double] =
{ case x if x > 0 => Math.log(x) }When you call a partial function on an input which isn’t defined, it throws a MatchError exception, which isn’t ideal as exceptions are unpredictable. Instead of using a partial function, you can define a total function that returns an optional value: this approach protects your code from an unexpected MatchError exception at runtime. For example, you could re-implement the function log as a total function as follows:def log(x: Int): Option[Double] = x match {
case x if x > 0 => Some(Math.log(x))
case _ => None
}
Unless you’re using an API or library which is explicitly requesting you to use partial functions, consider re-defining your partial functions as total functions to make your code safer at runtime by avoiding possible unpredicted exceptions.
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
.
Quick check 2: Write a function called greetings
that takes an optional custom message as its parameter and returns a string. When the optional parameter is defined (i.e. it contains a value) use it as greeting message, and when the optional parameter is missing use the predefined message “Greetings, Human!” For example, greetings(Some("Hello Scala"))
should return the string “Hello Scala,” and greetings(None)
should return “Greetings, Human!”
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!
Try this: Define a case class Person
to represent a person with a first name, an optional middle name, and a last name. Write a function that takes an instance of Person as its parameter, and returns a string describing its full name. For example, when representing a person whose first name is George, middle name is Watson, and last name is Lucas it should return the string “George Watson Lucas.” On the other hand, when representing a person whose first name is Martin, has no middle name, and last name is Odersky, it should return “Martin Odersky.”
Answers to Quick Checks
Quick check 1A possible implementation for the function filter is the following:def filter(text: String, word: String): Option[String] =
if (text.contains(word)) Some(text) else NoneQuick check 2You could implement the function greetings as follows: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.
Originally published at https://freecontent.manning.com.