Option, Some, None in Scala (OR) How to handle null values in Scala?

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 OptionSome, and None. The Some and None classes are subclasses of Option, so the solution works like this:

  • You declare that toInt returns an Option type
  • If toInt receives a string it can convert to an Int, you wrap the Int inside of a Some
  • If toInt receives a string it can’t convert, it returns a None

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 a Some[Int], i.e., an integer wrapped inside a Some
  • If any of the three strings can’t be converted to an inside, y will be a None

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 it
  • None 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 mapfilterforeach, 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 the Some class knows how to reach inside the Some container and extract the value (1) that’s inside it, so it passes that value to println

Similarly, the second example prints nothing because:

  • toInt("x") evaluates to None
  • The foreach method on the None class knows that None 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

Leave a Reply

Your email address will not be published. Required fields are marked *