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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def talkAboutString(str: String) { | |
Console.println("value is the String \""+str+"\", length of "+str.length) | |
} | |
def talkAboutValue(value: Option[String]) { | |
value match { | |
case Some(str) => talkAboutString(str) | |
case None => Console.println("value is None") // A compile-time warning will appear if "None" is not handled. | |
} | |
} | |
// Sample usage: | |
Console.println( | |
"Getting an Option that happens to hold something...") | |
talkAboutValue(getSomeString) | |
Console.println( | |
"\nGetting an Option that happens to hold nothing...") | |
talkAboutValue(getNoString) | |
/* | |
Output: | |
Getting an Option that happens to hold something... | |
value is the String "test", length of 4 | |
Getting an Option that happens to hold nothing... | |
value is None | |
*/ |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def firstToLastNameMap : Map[String,String] = Map( | |
"beth"->"peterson", | |
"harry"->"smith", | |
"john"->"doe" | |
) | |
def retrieveLastNameFromMap(firstName: String) = { | |
firstToLastNameMap.get(firstName) | |
} | |
// Sample usage | |
Console.println("\nRetrieving last name of harry...") | |
talkAboutValue(retrieveLastNameFromMap("harry")) | |
Console.println("\nRetrieving last name of ben...") | |
talkAboutValue(retrieveLastNameFromMap("ben")) | |
/* Output | |
Retrieving last name of harry... | |
value is the String "smith", length of 5 | |
Retrieving last name of ben... | |
value is None | |
*/ |
Fortunately, Scala alleviates this via facilities in its core API. The most obvious is "getOrElse".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Sample usage | |
Console.println("Last name of harry is: "+ | |
firstToLastNameMap.get("harry").getOrElse("not found")) | |
Console.println("Last name of harry is: "+ | |
firstToLastNameMap.get("sam").getOrElse("not found")) | |
/* | |
Output: | |
Last name of harry is: smith | |
Last name of harry is: not found | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def listOfOptions : List[Option[String]] = | |
List(Some("first"), None, Some("third"), None, | |
Some("fifth")) | |
listOfOptions.foreach(talkAboutValue(_)) | |
/* | |
Output: | |
value is the String "first", length of 5 | |
value is None | |
value is the String "third", length of 5 | |
value is None | |
value is the String "fifth", length of 5 | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Console.println("Flattened list is "+listOfOptions.flatten) | |
/* | |
Output: | |
Flattened list is List(first, third, fifth) | |
*/ |
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.toListScala, 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Console.println("\nLast names for list of first names:") | |
Console.println(List("beth","sam","harry").flatMap( | |
firstToLastNameMap.get(_)).map("["+_+"]").mkString(",")) | |
/* | |
Output: | |
Last names for list of first names: | |
[peterson],[smith] | |
*/ |
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