b-studios

Scala: Implicit Optionals

A few days ago I stumbled apon Paul Phillips awesome talk “Inside the Sausage Factory” – explaining some internals of the Scala compiler. In his talk he mentions that he does not like using Option since it is “tedious and syntax heavy”. He suggests to use the “null object pattern” – creating a “distinguishable object to server the role of none”.

A problem with this approach is that one looses all the nice methods Option provides such as getOrElse or map. In this post we will see how we can get the best of both worlds by using implicits.

Let’s start with a small motivating example, inspired by the scala compiler implementation.

sealed trait Symbol { def name: String }
case class MySymbol(name: String) extends Symbol
case object NoSymbol { def name = "<NoSymbol>" }

Using this pattern allows us to check for x == NoSymbol and use x otherwise without having it to unwrap via get or an extractor.

But what if we would like to use orElse on a Symbol defined above:

val x: Symbol = MySymbol("foo")
x orElse MySymbol("bar")
// BANG! error: value orElse is not a member of Symbol

This problem could of course be fixed by adding method orElse to Symbol and implementing it in MySymbol and in NoSymbol:

sealed trait Symbol {
  def orElse[T >: Symbol](default: => T): T
}
case class MySymbol(name: String) extends Symbol {
  def orElse[T >: Symbol](default: => T): T = this
}
case object NoSymbol {
  def orElse[T >: Symbol](default: => T): T = default
}

Hey, this works. But it replicates code from scala.Option. Maybe it is a better idea to inherit from Option then?

sealed trait Symbol extends Option[Symbol] { ... }
// BANG! illegal inheritance from sealed class Option

Nope. Option is sealed, so there is no way to inherit from it. We need to find another way…

Implicits to the Rescue

Implicits can be used to add methods to an existing library in retrospective. Why not pimp symbols with the methods of Option?

implicit def noSymbol2Option(sym: NoSymbol.type): Option[Symbol] = None
implicit def symbol2Option(sym: Symbol): Option[Symbol] = sym match {
  case NoSymbol => None
  case _ => Some(sym)
}

The singleton type of NoSymbol is more specific than Symbol and so the first implicit conversion is preferred over the second – if the singleton type is known at compile time. In all other cases we have to add a runtime check in order to convert to Option[Symbol]. Of course the first conversion also could be omitted, since it may not often be known at compile time whether sym is of singleton type NoSymbol.type or not. It is just a little optimization trying to use compile time information.

The above example usage now works without modification. Not changing or duplicating existing code we are able to handle Symbol the same way as Option while at the same time staying as concise as Paul Phillips likes it :)

Comments