Monday 6 September 2010

Option in Scala vs null in Java

When I first came to Scala a few months ago, one of the things I'd heard was to avoid using null and use the Option class instead.

Great, I thought. So instead of:

String x = ...
if (x != null) {
  doSomething(x);
}

I'd write:

val x: Option[String] = ...
if (x.isDefined) {
  doSomething(x.get)
}

Or perhaps:

val x: Option[String] = ...
x match {
  case Some(x1) => doSomething(x1)
  case None => // do nothing
}

Much as I was loving scala, neither of these were a great advance forward. In fact, even without Java's semicolon, they're both still more characters to type, and certainly not aiding readability.

Until one day, with the help of my colleague Daithi, the realisation came: treat an Option as a collection with zero or one entries.

So now I'd write:

val x: Option[String] = ...
x.map(doSomething(_))

("map", defined for all collections, creates a new collection with each element transformed using the function supplied. For Options, this means None => None, and Some(a) => Some(f(a)).  "map" is another one of those methods that my java background couldn't see the point of, but now I find myself using over and over again.  More on this in a later post, perhaps.)

For a real life example, take a look at the scala client for the Guardian's open platform content api.  This code comes from Api.scala, which is a simple builder for the content api url:

trait PaginationParameters extends Parameters {
    var _pageSize: Option[Int] = None
    var _page: Option[Int] = None

    def pageSize(newPageSize: Int): this.type = {
      _pageSize = Some(newPageSize); this
    }

    def page(newPage: Int): this.type  = {
      _page = Some(newPage); this
    }

    override def parameters = super.parameters ++
            _pageSize.map("page-size" -> _) ++
            _page.map("page" -> _)
    }
}

ExampleUsageTest shows this in action.

The interesting code is in the override of parameters, which returns a Map[String, Any] of the query string parameters to pass to the api. Each Option is mapped to a tuple of the parameter name and its value. This is then added onto the map returned by the base class using ++ which returns a new map with the tuple added.

Map[String, Any].++ expects to be passed a TraversableOnce[(String, Any)], i.e. a collection of String -> Any tuples.  Since the _pageSize.map returns either None or Some("page-size" -> value) and we can treat an option as a collection with zero or one entries, we end up either adding nothing to the map (None) or adding a "page-size" -> value to the map.

A relatively long explanation, mostly because as an (ex-?)Java dev I still sometimes have to understand exactly what's going on underneath.  When I first started developing C, I had to understand what machine code was generated. When I first started developing C++, I had to understand what C was generated.  Luckily both wore off, and I expect it will happen again soon.

But read the resulting code. It's concise, and its intent is clear. And hey, no null checks!




NB: After writing most of this post, I found an excellent, far better written, write up of using Option sensibly here.  Tony's scala Option cheat sheet is worth a read too.

No comments: