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 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 :)