Monday, November 23, 2009

Error detection with pattern matching

Scala's pattern matching is one of the most powerful features in the language. It not only helps write concise and very readable code, but also helps prevent trivial errors.

Let's say you want to save a line or two of code when comparing for some empty data structure like a List. You decide to use comparison for equality:


val items = Nil
if (items == Nil) println("No items")


Then you decide to refactor and turn the items collection into a Set. The Scala compiler is clever and should give you a warning, right? Well, not quite:


val items = Set()
if (items == Nil) println("No items")


This results in nothing printed, as the expression evaluates to false. Of course, given the types it's perfectly clear at compile-time that this will never be true. Can't the compiler give you a hint? Indeed, it will if you use pattern matching:


items match {
case Nil => println("No items")
case _ =>
}


This will result in the following error message:


error: pattern type is incompatible with expected type;
found : object Nil
required: scala.collection.immutable.Set[Nothing]
case Nil => println("No items")
^


The examples are contrived (unparameterized Nil and Set?), but you get the point. Yes, Nil can never be a value of the Set type. You might think this is fairly obvious, but pattern matching can be a life saver when implicit conversions are involved. Consider this:


"heh".reverse == "heh"


What does this evaluate to? This should be obvious, right? But the value is false! If you used pattern matching, you would easily see why:


"heh".reverse match {
case "heh" => "obvious?"
}


This will make the compiler very nervous, and this is the reason why:


error: type mismatch;
found : java.lang.String("heh")
required: scala.runtime.RichString
case "heh" => "obvious?"
^


So reverse converts the String to the wrapper RichString, which is not the same type as String.

I have had similar problems detecting a bug where I was checking for equality with None a variable which was of type net.liftweb.common.Box (a type very similar conceptually to Scala's built-in Option).

This made me adopt a general rule to prefer pattern matching rather than equality comparison. The bugs it catches are sometimes subtle and hard to see, and that's exactly what Scala's rich static type checker tries to avoid. Use it to your advantage.

Since we're talking about bugs caught by pattern matching, there's one subtle bug, which is often (though not always) caught by the compiler (another contrived example follows):


val items = Set()
Set() match {
case items => println("empty")
case _ => println("full")
}


This will result in an error, which looks a little bit unusual to the newbie:


error: unreachable code
case _ => println("full")
^


This error is usually crying out loud: hey, you're inadvertently using variable binding instead of constant matching! The newly bound variable items shadows the existing variable with the same name and all other cases after it will never match.

One way to fix it is to use backticks to prevent the name to be bound to a new variable:


Set() match {
case `items` => println("empty")
case _ => println("full")
}


As a rule of thumb it is advised to use CapitalLetters for case classes and constants which you intend to pattern match.

This error wouldn't have occurred if you used equality comparison in the first place, but even in mildly complex cases pattern matching trumps plain equality checking in readability and detecting errors. Apparently, there are cases where pattern matching fails (for instance, matching structural types), so there's still no reason to deprecate good old "==". But there are many more errors, which pattern matching catches, like checking if the match is exhaustive. So there's no point in saving a couple of characters but lose the type safety you expect from Scala.

6 comments:

Squirrels Ewer said...

Have you tried scalaz's "type-safe equals" (===)? I haven't, but it looks promising.

Spiros said...

"heh".reverse match {
case "heh" => "obvious?"
}

this works as expected in Scala 2.8.

Sara Reid said...

Nowadays, many defects, e.g., obscure error generation-scenario and lacking of formalization which is the basis for the automatic error detection, exist in field of code error research. Furthermore, the automation of error detection will greatly affect the quality and efficiency of software testing. Therefore, more deeply research on code errors need to be done.

discount supplements

Vassil Dichev said...

@Squirrels Ewer
I had problems making scalaz work in Scala 2.7.7 and 2.8 RC1, but looks like it should work, it uses an implicit to convert to Equal, and then uses the type-safe method order(a1: A, a2: A). Although I find the conflict with the same operator name from ScalaTest a bit unfortunate, I think this is the way to go.

@Spiros
True, in Scala 2.8 "reverse" works, because the String is converted implicitly to a WrappedString, where "reverse" returns String. This will not help with the general problem where you're comparing an implicitly converted object with an object of the original type.

Vassil Dichev said...

OK, Scalaz 4.0 Pre-Release 1 works great with 2.7.7, not only does === warn if the types don't match, but it also converts one of the arguments if there's an available implicit conversion, which matches.

Flying Frog Consultancy Ltd. said...

This looks less like a success of pattern matching and more like a failure of equality to me. Your incorrect code would have been caught as a type error in most other languages.