Another tour of Scala

 
 
 
 

Case Classes

Viewing old version 5d36d5ae521db0f8da06c981450a90e2d535faa2; View Current

The Gist

Case Classes delves deeper into the match and switch power in Scala, and also describes some seemingly disparate ways of saving you a lot of lines of code. Extractor Objects explains how the values are extracted.

My Interpretation

Suppose you are creating a logging system like log4j that can log a String, an Exception, or both. Suppose further that you wish to keep stack traces to a minimum and only output them for Error exceptions that are logged, or for any exception logged at the error level.

There are several possible means of implementing this in Java. Here is one such implementation:

There’s a lot of repetition here, and the conciseness of the log method can make it a bit hard to grok at first.

Scala’s case classes provide a lot of features to help. Here’s a possible implementation in Scala:

The simple addition of the case keyword before the three extenders of the log message class does a whole lot:

  • properties to access the constructor arguments are created automatically
  • an equals method based on the constructor arguments is created
  • a toString implementation is provided that uses the constructor arguments
  • instances can be created without the new operator, just the classname is needed
  • the constructor arguments can be extracted in one statement, as we do in the match block

The match block itself also packs quite a bit of functionality. Think of it as a switch statement on steroids. In this case, we are “switching” on, among other things, the type of l, the log message. Each case expression allows us to implement a function that uses the extracted constructor arguments.

The first case statement executes if l is of type StringMessage, and, if so, “pulls out” the message that was passed to its constructor.

We can also match a case statement with conditions beyond just the type of l. The second case statement is executed if l is an ExceptionMessage and if the extracted argument from l is of type Error. If l is an ExceptionMessage, but was not created with an exception that is a subclass of Error, we don’t match and proceed to the next case to check for a match.

The next statement will match if l is of type ExceptionMessage and if l ’s exception argument was any type of Throwable,however there is a guard condition; if the log level passed to log is less than 5, we match and execute this code. If not, we proceed looking for another match.

The subsequent statement will match any other ExceptionMessage that wasn’t matched by the first two (because we have no type on the argument, and no guard conditions).

This may sound complicated in explanation, but look back at the code. It’s actually pretty clear what’s going on now that you’ve got some hints as to the meaning of the syntax.

We’ve simplified some complex logic by reducing line count and repetition, and we’ve also avoided having to have nine different methods to cover all of our cases.

Now look back at the Java code. We certainly could have implemented our Java logger using a similar log message class hierarchy to reduce those nine methods to three, but the required classes would require several lines of code each (compared to Scala’s one) and would not have the ability to extract their arguments so concisely.

Like ScalaFunctions, case classes are far more powerful than what is shown here, but this shows you another means of simplifying your codebase.

Extracting Values

You’ll notice in the various case clauses that the values are extracted out of the class. This is done via a method called unapply, which is a huge amount of syntactic sugar. In the most comprehendable case (and the case here, with case-classes), unapply takes the arguments of types given to the constructor and returns an Option. Option is a class that either has a value or has no value (None). This is how Scala determines if a match is made. If the return from unapply is an Option that does not have the value None, a match is considered to have been made and you can access the values from the option (Something like Option[(Int,String,Int)] would be the type of @apply@’s return value for a class that took an Int, a String, and an Int in its constructor). If @unapply@’s option returns None, a match is not considered to have been made and processing continues.

My Thoughts on this Feature

It’s certainly not remotely intuitive that putting case before a class definition would give you the automatic stuff it gives you. I want this stuff outside of ever needing to use a class in a case statement, and so it just seems weird to call it “case classes”.

That being said, it’s pretty darn useful and anything that saves me from typing the same code over and over is a win. The “deconstruction” bit is also pretty cool, even if the underpinnings of how it works (not discussed here) are hard to follow.

unapply is highly subtle and a really huge bit of hand-waving, but is obviously the way in which the case class stuff has to be implemented. It’s hard to come up with a use for it outside of this, and I’m not clear on when you’d write your own instead of just relying on the case modifier.


Last Updated 07/31/2009 at 03:26:07 PM by davec

blog comments powered by Disqus