Forcing Scala Compiler 'Nothing' Checks
by Brendan McAdams
Since early in its history, Casbah has had a helper method called
T is “Some type you’d like to fetch a particular field as”. Because of type erasure on the JVM, working with a Mongo Document can be annoying – the representation in Scala is the equivalent of a
Map[String, Any]. If we were to work with the
Map[String, Any] in a standard mode, fetching a field balance which is a
Double would require manual casting.
We have already hit another issue here – in Scala, invoking
get on a
Option[T] (Where, in this case,
T is of type
Any). Which means casting has become more complex: to get a
Double we also have to unwrap the
Option[Any] first. A lazy man’s approach might be something hairy like so:
In the annals of history (when men were real men, and small furry creatures from Alpha Centauri were real small furry creatures from Alpha Centauri), the above became an annoyingly common pattern. A solution was needed - and so
getAs[T] was born. The idea was not only to allow a shortcut to casting, but take care of the
Option[T] wrapping for you as well. Invoking
getAs[Double] will, in this case, return us an
But not everything is perfect in the land of
getAs[T] – if the type requested doesn’t match the actual type, runtime failures occur. Worse, if the user fails to pass a type, the Scala compiler substitutes
Nothing, which guarantees a runtime failure. Runtime failures are bad – but fortunately, Miles Sabin & Jon-Anders Teigen came up with an awesome solution.
We get back an option of
Nothing, which is less than ideal (The REPL appears to be somewhat more forgiving in some of this behavior than the actual runtime is). My reaction to this early on was quite strong –– I wanted to require that the user pass their type argument. Unfortunately, the best I could do within Casbah was attempt to detect the compiler substituted
Nothing and warn the user at runtime. Less than ideal, I know.
This gave me somewhat improved behavior –- at least users are warned at runtime before something breaks.
Great – we prevent people from utterly failing to pass a type to
getAs by throwing an exception at runtime. A bit like closing the barn doors after the horses escaped, and somewhat counter to the point of compiled languages. Fortunately, Miles Sabin knows a lot of great compiler tricks and Jon-Anders has superpowers (which he uses for good, not evil). Using some of Miles’ tricks, Jon-Anders has fixed Casbah (as of 2.3.0+) to make
getAs[T] fail utterly at compile time when no type is passed.
The secret to this trick is essentially that the Scala compiler hates ambiguity. In order to substitute
Nothing as a type argument when one isn’t supplied, the Scala compiler has an implicit for
Nothing scoped. If one were to exacerbate the situation by introducing an additional implicit for
Nothing, the compiler would fail when no type argument is passed.
With this in mind, we can morph
getAs to work with a type class instead of a standard type argument.
Our previous unbounded type argument is replaced with the new type class boundary of
NotNothing and the runtime
Nothing check is removed. We also need concrete instances of our type class, which is where the real magic comes into play.
Now, any application of
Nothing will trigger the ambiguity problem – the Scala compiler won’t figure out how to resolve the type argument. This trick works because
Nothing is at the bottom of Scala’s type hierarchy. Were I to call
getAs("balance"), the Scala compiler would attempt to fill in
Nothing as the type argument. However, both implicit conversons for
notNothing[A] will match – causing ambiguity and compilation fails.
A vast improvement in behavior, especially if we use the
@implicitNotFound annotation to provide clear error messages.
The moral of the story – knowing the ins and outs of the type system and compiler corners can do great things for improving the functionality of your code. Especially being aware that as smart as the Scala compiler is, there are limitations inherent in the runtime platform (the JVM, specifically type erasure) that can make our lives difficult if ignored.
While reviewing a draft of this post, Daniel Spiewak noted one more issue with my code as it exists. Namely, that we don’t have a sane way of preventing users from miscasting. That is to say, if I try to fetch “balance” as a
String, this shouldn’t be OK.
Daniel rightly points out how bad a runtime
ClassCastException is, and has proposed another fix which I’m incorporating.
Now, when you ask for a type that doesn’t match what the Document contains, you will receive
None and a warning in your log such as
Unable to cast 'java.lang.Double' as 'java.lang.String'; please check your types..