Sponsored By

Opinion: Never Say Null Again

In this reprinted <a href="http://altdevblogaday.com/">#altdevblogaday</a>-opinion piece, Mind Candy senior developer Sean Parsons attempts to help coders never see another NullPointerException (or its equivalent) again with Scala.

Sean Parsons, Blogger

September 30, 2011

7 Min Read
Game Developer logo in a gray background | Game Developer

[In this reprinted #altdevblogaday-opinion piece, Mind Candy senior developer Sean Parsons attempts to help coders never see another NullPointerException (or its equivalent) again with Scala.] Goldfinger Don't take it from me, but even the man who gave us null wishes he didn't. Probably one of the most recognizable patterns of code any programmer will write is something like the following:

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 Maybe type in Haskell and Option in Scala. In the case of Scala, Option is an abstract class that has only two subclasses: Some which contains a value; 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:

  • Identifies the user ID from a cookie.

  • If the user Id is found, lookup user information for that user.

  • If the user information for that user is found, transform that user information into JSON.

  • Otherwise, return some default JSON.

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. As ever, experiment with all the code in here on Simply Scala, and I hope you never see any NullPointerException's (or the equivalent) ever again! [This piece was reprinted from #AltDevBlogADay, a shared blog initiative started by @mike_acton devoted to giving game developers of all disciplines a place to motivate each other to write regularly about their personal game development passions.]

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like