This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 20.5, “Scala best practice: Eliminate null values from your code.”
Problem
Tony Hoare, inventor of the null
reference way back in 1965, refers to the creation of the null
value as his “billion dollar mistake.” In keeping with modern best practices, you want to eliminate null
values from your code.
Solution
David Pollak, author of the book Beginning Scala, offers a wonderfully simple rule about null
values:
“Ban
null
from any of your code. Period.”
Although I’ve used null
values in this book to make some examples easier, in my own practice, I no longer use them. I just imagine that there is no such thing as a null
, and write my code in other ways.
There are several common situations where you may be tempted to use null
values, so this recipe demonstrates how not to use null
values in those situations:
- When a
var
field in a class or method doesn’t have an initial default value, initialize it withOption
instead ofnull
. - When a method doesn’t produce the intended result, you may be tempted to return
null
. Use anOption
orTry
instead. - If you’re working with a Java library that returns
null
, convert it to anOption
, or something else.
Let’s look at each of these techniques.
Initialize var
fields with Option
, not null
Possibly the most tempting time to use a null
value is when a field in a class or method won’t be initialized immediately. For instance, imagine that you’re writing code for the next great social network app. To encourage people to sign up, during the registration process, the only information you ask for is an email address and a password. Because everything else is initially optional, you might write some code like this:
case class Address (city: String, state: String, zip: String) class User(email: String, password: String) { var firstName: String = _ var lastName: String = _ var address: Address = _ }
This is bad news, because firstName
, lastName
, and address
are all declared to be null
, and can cause problems in your application if they’re not assigned before they’re accessed.
A better approach is to define each field as an Option
:
case class Address (city: String, state: String, zip: String) class User(email: String, password: String) { var firstName = None: Option[String] var lastName = None: Option[String] var address = None: Option[Address] }
Now you can create a User
like this:
val u = new User("al@example.com", "secret")
At some point later you can assign the other values like this:
u.firstName = Some("Al") u.lastName = Some("Alexander") u.address = Some(Address("Talkeetna", "AK", "99676"))
Later in your code, you can access the fields like this:
println(firstName.getOrElse("<not assigned>"))
Or this:
u.address.foreach { a => println(a.city) println(a.state) println(a.zip) }
In both cases, if the values are assigned, they’ll be printed. With the example of printing the firstName
field, if the value isn’t assigned, the string <not assigned>
is printed. In the case of the address
, if it’s not assigned, the foreach
loop won’t be executed, so the print statements are never reached. This is because an Option
can be thought of as a collection with zero or one elements. If the value is None
, it has zero elements, and if it is a Some
, it has one element — the value it contains.
On a related note, you should also use an Option
in a constructor when a field is optional:
case class Stock ( id: Long, var symbol: String, var company: Option[String] )
Don’t return null
from methods
Because you should never use null
in your code, the rule for returning null
values from methods is easy: don’t do it.
This brings up the question, “If you can’t return null
, what can you do?”
Answer: Return an Option
. Or, if you need to know about an error that may have occurred in the method, use Try
instead of Option
.
With an Option
, your method signatures should look like this:
def doSomething: Option[String] = { ... } def toInt(s: String): Option[Int] = { ... } def lookupPerson(name: String): Option[Person] = { ... }
For instance, when reading a file, a method could return null
if the process fails, but this code shows how to read a file and return an Option
instead:
def readTextFile(filename: String): Option[List[String]] = { try { Some(io.Source.fromFile(filename).getLines.toList) } catch { case e: Exception => None } }
This method returns a List[String]
wrapped in a Some
if the file can be found and read, or None
if an exception occurs.
As mentioned, if you want the error information instead of a Some
or None
, use the Try/Success/Failure approach instead:
import scala.util.{Try, Success, Failure} object Test extends App { def readTextFile(filename: String): Try[List[String]] = { Try(io.Source.fromFile(filename).getLines.toList) } val filename = "/etc/passwd" readTextFile(filename) match { case Success(lines) => lines.foreach(println) case Failure(f) => println(f) } }
This code prints the lines from the /etc/passwd file if the code succeeds, or prints an error message like this if the code fails:
java.io.FileNotFoundException: Foo.bar (No such file or directory)
The “Null Object” Pattern
As a word of caution (and balance), the Twitter Effective Scala page recommends not overusing Option
, and using the Null Object Pattern where it makes sense. As usual, use your own judgment, but try to eliminate all null
values using one of these approaches.
A Null Object is an object that extends a base type with a “null” or neutral behavior. Here’s a Scala implementation of Wikipedia’s Java example of a Null Object:
trait Animal { def makeSound() } class Dog extends Animal { def makeSound() { println("woof") } } class NullAnimal extends Animal { def makeSound() {} }
The makeSound
method in the NullAnimal
class has a neutral, “do nothing” behavior. Using this approach, a method defined to return an Animal
can return NullAnimal
rather than null
.
This is arguably similar to returning None
from a method declared to return an Option
, especially when the result is used in a foreach
loop.
Converting a null
into an Option
, or something else
The third major place you’ll run into null
values is in working with legacy Java code. There is no magic formula here, other than to capture the null
value and return something else from your code. That may be an Option
, a Null Object, an empty list, or whatever else is appropriate for the problem at hand.
For instance, the following getName
method converts a result from a Java method that may be null
and returns an Option[String]
instead:
def getName: Option[String] = { var name = javaPerson.getName if (name == null) None else Some(name) }
Benefits
Following these guidelines leads to these benefits:
- You’ll eliminate
NullPointerException
s. - Your code will be safer.
- You won’t have to write
if
statements to check fornull
values. - Adding an
Option[T]
return type declaration to a method is a terrific way to indicate that something is happening in the method such that the caller may receive aNone
instead of aSome[T]
. This is a much better approach than returningnull
from a method that is expected to return an object. - You’ll become more comfortable using
Option
, and as a result, you’ll be able to take advantage of how it’s used in the collection libraries and other frameworks.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- Tony Hoare’s Billion Dollar Mistake
- The “Null Object Pattern”