December 4, 2008

Pattern Guards fooled by null

A sadness:

Pattern guards don't handle nulls well, and, also, introducing a pattern guard appears to make a default _ not match on null values:

scala> def guard_fails(s: String) {
| s match {
| case s: String if s == null => println("null")
| case s: String if s != null => println("s=" + s)
| case _ => println("default")
| }
| }
guard_fails: (String)Unit

scala> guard_fails("test")
s=test

scala> guard_fails(null)
scala.MatchError
at .guard_fails(:5)
at .(:6)
at .()
at RequestResult$.(:3)
at RequestResult$.()
at RequestResult$result()
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMeth...
scala>

scala> def guard_fails2(s: String) {
| s match {
| case s: String if s != null => println("s=" + s)
| case _ => println("default")
| }
| }
guard_fails2: (String)Unit

scala> guard_fails2("test")
s=test

scala> guard_fails2(null)
scala.MatchError
at .guard_fails2(:5)
at .(:6)
at .()
at RequestResult$.(:3)
at RequestResult$.()
at RequestResult$result()
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMet...
scala>

To code this match defensively, you have to define a null pattern explicitly:

scala> def ok(s: String) {
| s match {
| case s: String if true => println("s=" + s)
| case null => println("null")
| case _ => println("default")
| }
| }
ok: (String)Unit

scala> ok("test")
s=test

scala> ok(null)
null

scala>

4 comments:

Daniel Spiewak said...

Sounds like a bug to me. I would file it in Scala's Trac: https://lampsvn.epfl.ch/trac/scala

Jorge Ortiz said...

Ahh, that's a tricky little bugger. I think this behaves according-to-spec. (I omit judgement as to whether the spec's decision is good or bad.)

Section 8.2 of SLS on Type Patterns says: "A reference to a class C [...] matches any non-null instance of the given class."

So it's the String annotation here that's excluding the null from the match. In particular, the following code works as one would expect:

def guard(s: String) = s match {
__case p if p == null => "null"
__case p if p != null => p
}

martin said...

I half agree with Jorge. If you match with `case x: String =>`, null is excluded. That's just like null is always excluded in isInstanceOf tests (in both Java and Scala). So the program should have taken the third, default case. However, it seems that the inaccessible guard
`if x == null` fooled the pattern matcher. So it would be good if you could file a ticket on that.

Ricky Clarkson said...

Consider using Option instead of nulls.