Functional programming is like writing a series of algebraic equations, and because you don’t use null values in algebra, you don’t use null values in FP. Scala’s solution to handle null values is to use constructs like the Option/Some/None classes.
Imagine that you want to write a method to make it easy to convert strings to integer values, and you want an elegant way to handle the exceptions that can be thrown when your method gets a string like "foo"
instead of something that converts to a number, like "1"
. A first guess at such a function might look like this:
def toInt(s: String): Int = {
try {
Integer.parseInt(s.trim)
} catch {
case e: Exception => 0
}
}
The idea of this function is that if a string converts to an integer, you return the converted Int
, but if the conversion fails you return 0. This might be okay for some purposes, but it’s not really accurate.
Using Option/Some/None:
Scala’s solution to this problem 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
toInt
returns anOption
type - If
toInt
receives a string it can convert to anInt
, you wrap theInt
inside of aSome
- If
toInt
receives a string it can’t convert, it returns aNone
The implementation of the solution looks like this:
def toInt(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 integer wrapped in a Some
wrapper, such as Some(1)
. When the string can’t be converted to an integer, return a None
value.”
scala> val a = toInt("1")
a: Option[Int] = Some(1)
scala> val a = toInt("foo")
a: Option[Int] = None
Using a match expression
One possibility is to use a match
expression, which looks like this:
toInt(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 first case
statement is executed; if x
can’t be converted to an Int
, the second case
statement is executed.
Using for/yield
Another common solution is to use a for-expression — i.e., the for/yield combination that was shown earlier in this book. To demonstrate this, imagine that you want to convert three strings to integer values, and then add them together. The for/yield solution looks like this:
val y = for {
a <- toInt(stringA)
b <- toInt(stringB)
c <- toInt(stringC)
} yield a + b + c
When that expression finishes running, y
will be one of two things:
- If all three strings convert to integers,
y
will be aSome[Int]
, i.e., an integer wrapped inside aSome
- If any of the three strings can’t be converted to an inside,
y
will be aNone
You can test this for yourself in the Scala REPL. First, paste these three string variables into the REPL:
You can test this for yourself in the Scala REPL. First, paste these three string variables into the REPL:
val stringA = "1"
val stringB = "2"
val stringC = "3"
scala> val y = for {
| a <- toInt(stringA)
| b <- toInt(stringB)
| c <- toInt(stringC)
| } yield a + b + c
y: Option[Int] = Some(6)
Option can be thought of as a container of 0 or 1 items:
One good way to think about the Option
classes is that they represent a container, more specifically a container that has either zero or one item inside:
Some
is a container with one item in itNone
is a container, but it has nothing in it
Using foreach:
Because Some
and None
can be thought of containers, they can be further thought of as being like collections classes. As a result, they have all of the methods you’d expect from a collection class, including map
, filter
, foreach
, etc.
This raises an interesting question: What will these two values print, if anything?
toInt("1").foreach(println)
toInt("x").foreach(println)
The answer is that the first example prints the number 1
, and the second example doesn’t print anything. The first example prints 1
because:
- toInt(“1”) evaluates to
Some(1)
- The expression evaluates to
Some(1).foreach(println)
- The
foreach
method on theSome
class knows how to reach inside theSome
container and extract the value (1
) that’s inside it, so it passes that value toprintln
Similarly, the second example prints nothing because:
toInt("x")
evaluates toNone
- The
foreach
method on theNone
class knows thatNone
doesn’t contain anything, so it does nothing
Using Option to replace null values:
Another 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, that class is subject to this type of abuse:
val santa = new Address(
"1 Main Street",
null, // <-- D'oh! A null value!
"North Pole",
"Alaska",
"99705"
)
To handle situations like this, developers tend to use null values or empty strings, both of which are hacks to work around the main 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],
var city: String,
var state: String,
var zip: String
)
With that definition, developers can write more accurate code like this:
val santa = new Address(
"1 Main Street",
None,
"North Pole",
"Alaska",
"99705"
)
or this:
val santa = new Address(
"123 Main Street",
Some("Apt. 2B"),
"Talkeetna",
"Alaska",
"99676"
)
Once you have an optional field like this, you work with it as shown in the previous examples: With match
expressions, for
expressions, and other built-in methods like foreach
.
Other approaches:
The trio of classes known as Try/Success/Failure work in the same manner, but a) you primarily use these classes when code can throw exceptions, and b) the Failure
class gives you access to the exception message. For example, Try/Success/Failure is commonly used when writing methods that interact with files, databases, and internet services, as those functions can easily throw exceptions