The Time I Got Bit By a Convincing Contract
When you write code in a strongly typed, compiled language, you’re constantly making contracts. You are constantly telling the compiler that something has to be a certain way. When you tell the compiler that you want something to be a certain way, you expect that something to be that certain way.
Here’s the thing:
the inclusion of null
allows, well, a Null Pointer Exception
(NPE
).
As far as I’m concerned, this is the worst bug because it is a bug that should never happen.
An NPE
can happen at any time on any thing in Scala even though the language has baked-in functional programming features.
The possibility of sudden, unexpected failure is always there, lurking in the shadows.
I recently was bit an NPE
of my own fault.
Let’s walk through what happened.
What Happened?
Suppose I want some cache to store an Option[A]
with a String
for a key.
I can define a Guava cache in Scala like so.
import com.google.common.cache.CacheLoader
class SomeCacheLoader extends CacheLoader[String, Option[String]] {
override def load(key: String): A = {
Some(s"Loaded key: $key")
}
}
Now, let’s initizialize an instance of the loading cache.
import com.google.common.cache.{CacheBuilder, LoadingCache}
val someLoadingCache: LoadingCache[String, Option[String]] = CacheBuilder.newBuilder()
.maximumSize(10)
.build(new MyCacheLoader())
Here’s where things go to “hell in a handbasket”. Let’s attempt to retrieve an item from the empty cache.
val someKey = "Some Key"
val someValueFromCache = loadingCache.get(someKey)
Sanity Check
What’s the value of someValueFromCache
?
Is it None
or null
?
It seems perfectly reasonable upon a quick glance that it is None
because the return type of get
is an Option
.
Unfortunately, it’s actually a dreaded null
!
However, our return type of get
is T
because this is a Java function.
We used a Java function in Scala, and it’s very easy to forget this!
Our contract says that the returned value will be of type Option[String]
.
A get
on a Scala map for some map S -> T
returns an Option[T]
.
Since the T
in this example is an Option[String]
, we would expect to see an Option[Option[String]]
as the for the type of someValueFromCache
.
It’s perfectly reasonable for anyone to look at this code snippet quickly and think that nothing is wrong.
How Could This Have Been Prevented?
We had two solutions to avoid this:
- Use
getOrElse
on the cache withNone
specified as the default. This is my preferred solution because it avoids flattening. - Cast the result of the lookup to an
Option
(and be forced to flatten it).
Both of these solutions are perfectly accessible. Yet, if we don’t utilize them, we will have code that looks like it works but is actually dangerously broken. Now, that’s spooky 👻!
Takeaway
The safest way to not have a Null Pointer Exception
is to use a language in which null
does not exist.
Remember: just because the return type is an Option
, there is still a possibility of a Java function returning null
, and you absolutely must account for it!
There may be a contract, but null
makes all contracts null and void.