Functional programming is like writing a series of algebraic equations, and because algebra doesn’t have null values or throw exceptions, you don’t use these features in FP. This brings up an interesting question: In the situations where you might normally use a null value or exception in OOP code, what do you do?
Scala’s solution is to use constructs like the Option
/Some
/None
classes.
This lesson provides an introduction to using these techniques.
Two notes before we jump in:
- The
Some
andNone
classes are subclasses ofOption
. - Instead of repeatedly saying “
Option
/Some
/None
,” the following text generally just refers to “Option
” or “theOption
classes.”
A first example
While this first example doesn’t deal with null values, it’s a good way to introduce the Option
classes, so we’ll start with it.
Imagine that you want to write a method that makes it easy to convert strings to integer values, and you want an elegant way to handle the exception that’s thrown when your method gets a string like "Hello"
instead of "1"
.
A first guess at such a method might look like this:
def makeInt(s: String): Int =
try {
Integer.parseInt(s.trim)
} catch {
case e: Exception => 0
}
def makeInt(s: String): Int =
try
Integer.parseInt(s.trim)
catch
case e: Exception => 0
If the conversion works, this method returns the correct Int
value, but if it fails, the method returns 0
.
This might be okay for some purposes, but it’s not really accurate.
For instance, the method might have received "0"
, but it may have also received "foo"
, "bar"
, or an infinite number of other strings that will throw an exception.
This is a real problem: How do you know when the method really received a "0"
, or when it received something else?
The answer is that with this approach, there’s no way to know.
Using Option/Some/None
A common solution to this problem in Scala is to use a trio of classes known as Option
, Some
, and None
.
The Some
and None
classes are subclasses of Option
, so the solution works like this:
- You declare that
makeInt
returns anOption
type - If
makeInt
receives a string it can convert to anInt
, the answer is wrapped inside aSome
- If
makeInt
receives a string it can’t convert, it returns aNone
Here’s the revised version of makeInt
:
def makeInt(s: String): Option[Int] =
try {
Some(Integer.parseInt(s.trim))
} catch {
case e: Exception => None
}
def makeInt(s: String): Option[Int] =
try
Some(Integer.parseInt(s.trim))
catch
case e: Exception => None
This code can be read as, “When the given string converts to an integer, return the Int
wrapped inside a Some
, such as Some(1)
.
When the string can’t be converted to an integer, an exception is thrown and caught, and the method returns a None
value.”
These examples show how makeInt
works:
val a = makeInt("1") // Some(1)
val b = makeInt("one") // None
As shown, the string "1"
results in a Some(1)
, and the string "one"
results in a None
.
This is the essence of the Option
approach to error handling.
As shown, this technique is used so methods can return values instead of exceptions.
In other situations, Option
values are also used to replace null
values.
Two notes:
- You’ll find this approach used throughout Scala library classes, and in third-party Scala libraries.
- A key point of this example is that functional methods don’t throw exceptions; instead they return values like
Option
.
Being a consumer of makeInt
Now imagine that you’re a consumer of the makeInt
method.
You know that it returns a subclass of Option[Int]
, so the question becomes, how do you work with these return types?
There are two common answers, depending on your needs:
- Use a
match
expression - Use a
for
expression
Using a match
expression
One possible solution is to use a match
expression:
makeInt(x) match {
case Some(i) => println(i)
case None => println("That didn’t work.")
}
makeInt(x) match
case Some(i) => println(i)
case None => println("That didn’t work.")
In this example, if x
can be converted to an Int
, the expression on the right-hand side of the first case
clause is evaluated; if x
can’t be converted to an Int
, the expression on the right-hand side of the second case
clause is evaluated.
Using a for
expression
Another common solution is to use a for
expression—i.e., the for
/yield
combination that was shown earlier in this book.
For instance, imagine that you want to convert three strings to integer values, and then add them together.
This is how you do that with a for
expression and makeInt
:
val y = for {
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
} yield {
a + b + c
}
val y = for
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
yield
a + b + c
After that expression runs, y
will be one of two things:
- If all three strings convert to
Int
values,y
will be aSome[Int]
, i.e., an integer wrapped inside aSome
- If any of the three strings can’t be converted to an
Int
,y
will be aNone
You can test this for yourself:
val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for {
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
} yield {
a + b + c
}
val stringA = "1"
val stringB = "2"
val stringC = "3"
val y = for
a <- makeInt(stringA)
b <- makeInt(stringB)
c <- makeInt(stringC)
yield
a + b + c
With that sample data, the variable y
will have the value Some(6)
.
To see the failure case, change any of those strings to something that won’t convert to an integer.
When you do that, you’ll see that y
is a None
:
y: Option[Int] = None
Thinking of Option as a container
Mental models can often help us understand new situations, so if you’re not familiar with the Option
classes, one way to think about them is as a container:
Some
is a container with one item in itNone
is a container, but it has nothing in it
If you prefer to think of the Option
classes as being like a box, None
is like an empty box.
It could have had something in it, but it doesn’t.
Using Option
to replace null
Getting back to null
values, a place where a null
value can silently creep into your code is with a class like this:
class Address(
var street1: String,
var street2: String,
var city: String,
var state: String,
var zip: String
)
While every address on Earth has a street1
value, the street2
value is optional.
As a result, the street2
field can be assigned a null
value:
val santa = new Address(
"1 Main Street",
null, // <-- D’oh! A null value!
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
null, // <-- D’oh! A null value!
"North Pole",
"Alaska",
"99705"
)
Historically, developers have used blank strings and null values in this situation, both of which are hacks to work around the root problem: street2
is an optional field.
In Scala—and other modern languages—the correct solution is to declare up front that street2
is optional:
class Address(
var street1: String,
var street2: Option[String], // an optional value
var city: String,
var state: String,
var zip: String
)
Now developers can write more accurate code like this:
val santa = new Address(
"1 Main Street",
None, // 'street2' has no value
"North Pole",
"Alaska",
"99705"
)
val santa = Address(
"1 Main Street",
None, // 'street2' has no value
"North Pole",
"Alaska",
"99705"
)
or this:
val santa = new Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
val santa = Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
Option
isn’t the only solution
While this section focuses on the Option
classes, Scala has a few other alternatives.
For example, a trio of classes known as Try
/Success
/Failure
work in the same manner, but (a) you primarily use these classes when your code can throw exceptions, and (b) you want to use the Failure
class because it gives you access to the exception message.
For example, these Try
classes are commonly used when writing methods that interact with files, databases, and internet services, as those functions can easily throw exceptions.
A quick review
This section was long, so let’s give it a quick review:
- Functional programmers don’t use
null
values - A main replacement for
null
values is to use theOption
classes - Functional methods don’t throw exceptions; instead they return values like
Option
,Try
, orEither
- Common ways to work with
Option
values arematch
andfor
expressions - Options can be thought of as containers of one item (
Some
) and no items (None
) - Options can also be used for optional constructor or method parameters