Monday, June 17, 2013

The Option Design Pattern

When starting to learn Scala, front and center was the utility of the Option design pattern.  I think it's useful, but at first it's fairly unwieldy, and it's much more useful when you know how to reasonably work with it.
The problem the Option pattern attempts to solve is the frequency of the NPE (null pointer exception), certainly a constant thorn in the side of every Java programmer. The problem is that you're mixing the valid range for a value with something that is invalid, "null". By allowing this combination, anyone who uses your provided value must be aware and provide accommodation for the possibility that this "I am invalid!" placeholder can occur.  There are no safeguards indigenous to a language such as Java, so if you forget to handle this, your software can exhibit an error at runtime.
The Option pattern removes the placeholder from the range of possible values by wrapping it in an "Option" object. This object can be one of two derivative classes: Some, or None.  If it's a "Some" object, it has a value that's guaranteed to be in the valid range.  If it's a None object, it represents the "absence of a valid value".
Scala cleanly handles Option objects via pattern matching.  For example, talkAboutValue is a simple method that takes an Option object, and displays a value if it's something, and doesn't attempt to display a value if it's nothing:

Scala displays a compile-time warning if None is not handled in talkAboutValue's pattern matching, encouraging comprehensive handling of the input. The input for the talkAboutString method is not an Option, so it's expected that the input will be a valid String.  This allows the programmer to confidently call the length method, without worrying about an NPE.  You can still input a null to talkAboutString, and handle the null value the Java-way (e.g. if(str==null) {... ) but Scala discourages this.
A very inescapable instance where Scala makes use of the Option object is with its implementation of Maps.  When you query a value, it's either in the map or it's not.  If it's not, rather than returning a null value like you may have been accustomed to in a language like Java, Scala returns a value of None.

Unfortunately, even for a lookup resulting in a valid value, you still have the Option object "wrapper" to deal with.  That means you have to do not only the "get" for the lookup, but another to get the actual value. An "if(val != null)" seems much easier than doing a pattern match every time.
Fortunately, Scala alleviates this via facilities in its core API. The most obvious is "getOrElse".

This doesn't alter the handling of the value if a lookup is unsuccessful, but it does at least provide a default value, which may be appropriate in many situations such as a default configuration setting. Another very common use case is handling a list of Options resulting from iterating over a collection:
It's not so bad in this single-use instance, but if you plan on using a collection of Options repeatedly, you may wish to preprocess it, to remove the wrapper objects:

Flatten results in a list of just the unwrapped values in the Some objects.

Flatten is the same method that can concatenate the lists in a list to a single list. E.g. List(List(1,2),List(3,4)).flatten results in List(1,2,3,4). If you care, the way flatten is able to operate on a list of Options is because of an implicit method, option2Iterable. An option can be converted to a list of 0 or 1 elements (0 for None, 1 for Some) with its toList method. This implicit method is called by flatten, resulting in the same treatment as a list of lists:
implicit def option2Iterable[A](xo: Option[A]): Iterable[A] =
  xo.toList
Scala, despite its infamously steep learning curve, is beautiful in the way that its implicits provide for such ease of use for common programming tasks. Along those lines, it's also common for one to have a list of values that must be passed to a method which results in an Option. If you only want to handle the iterations with successful results, such as values successfully retrieved from a map, there is another accommodation called "flatMap", which replaces a sequence of flatten then map.

You can see that the function passed to flatMap is only run on the last names that are successfully found in the map.

Two more nifty things to know.  If you pass a Java object to the constructor of Option, it will automatically wrap non-null values with Some() and null values are replaced by None. For instance, try going to your Scala REPL and type:
Option(null)

Now try:
Option(3)

And lastly, just to demonstrate how easy working with Options can be, please check out Tony Morris's nifty cheat sheet.  If you notice yourself handling an Option in a certain way, there's a very good chance Scala, or at least the scalaz library, provides you a shortcut you can replace it with:

Conclusion
In summary, I agree the Option pattern seems to get in the way when you're starting out with Scala, but in the end, it results in much safer code, free from one of the most ruthlessly frequent runtime errors of our time. Scala makes it not only available, but also easy to work with, so learn to love it!

No comments:

Post a Comment