Goldfinger.
Don’t take it from me, but even the man who gave us null wishes he didn’t: if (userId != null) {
UserDetails userDetails = lookupUserDetails(userId);
...
}
Goldfinger.
Don’t take it from me, but even the man who gave us null wishes he didn’t: if (userId != null) {
UserDetails userDetails = lookupUserDetails(userId);
...
}
Also that pattern might nest multiple times and with each subsequent level cyclomatic complexity increases. It’s also worth considering the complexity this introduces especially when carrying over the complexity from calling methods.
For Your Eyes Only.
Breaking down what is happening inside the if statement there’s a transform from the userId value to the userDetails value, which sounds functional in at least the most basic sense. Fundamentally there’s two steps in the code, one checks if the value is present and if it is the second transforms it. It turns out that someone has already solved this problem with the None which indicates that the value is not present.
So if we modify our example a little bit to use Option we end up with code looking like this:
if (userId.isDefined) { val userDetails: Option[UserDetails] = lookupUserDetails(userId) … }
In this case userId is an Option of Int but this isn’t a huge improvement. However, there’s a method on Option that can be taken advantage of to end up with code that looks like this instead:
val userDetails: Option[Option[UserDetails]] = userId.map(id => lookupUserDetails(id))
The closure in the map method is only invoked if userId contains a value, which satisfies the two requirements specified above.
That’s moderately better, but there are nested Option classes, as we’re transforming the value inside the Option. Instead of map, we should call flatMap instead, which returns the result of the transformation directly or None if is already None:
val userDetails: Option[UserDetails] = userId.flatMap(id => lookupUserDetails(id))
The World Is Not Enough.
Expanding on what we have, it’s easy to build a service that does the following:
With some token implementations here’s how that might look:
case class HttpRequest() case class UserDetails(id: Int, name: String) def getCookie(request: HttpRequest): Option[Int] = Some(9) def lookupUser(userId: Int): Option[UserDetails] = Some(new UserDetails(9, "Sean")) def toJSON(user: UserDetails): Option[String] = Some("""{"id":9, "name":"Sean","found":"true"}""") def userDetailsService(request: HttpRequest): String = { getCookie(request).flatMap(userId => lookupUser(userId)).flatMap(user => toJSON(user)).getOrElse("""{"found":"false"}""") } // There’s an assumption the code would then be called like this: userDetailsService(new HttpRequest()) // Alternatively the service can be written like this using a for-comprehension: def userDetailsService(request: HttpRequest): String = { val jsonOption = for { userId <- getCookie(request) user <- lookupUser(userId) json <- toJSON(user) } yield json jsonOption.getOrElse("""{"found":"false"}""") }
The benefit here is that at each level if any of the methods in userDetailsService above returned a None, then the whole expression returns a None and no exception is thrown at all.
You Only Live Twice.
Integrating with existing code that can return null need not preclude the use of Option, as the apply method on the Option companion object returns a None when the parameter is null or a Some containing the value if it is not:
val willBeNone = Option(null) val willBeSome = Option(9)
Also exceptions can be handled and turned into a None by using methods on the Exception object:
val willBeNone = Exception.allCatch.opt("Flange".toInt) val willBeSome = Exception.allCatch.opt("10".toInt)
Note that both of these values will be correctly typed to Option[Int] by the type inference as well.
The Spy Who Loved Me.
Another way to think about Option is that of a collection that can only hold 0 or 1 elements. The methods map, flatMap and foreach all works similarly between Set, List and Option. Which has the side benefit that it’s possible to start with an Option containing a singular item and change that later to a List if the need arises with minimal changes.
The idiomatic way to use Option as we’ve seen here is to map/flatMap as many times as necessary and call the get or foreach methods at the very end of the chain. Even for those languages that lack an Option like type it can be easy to write one that saves effort and helps to reduce complexity and bugs.