Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java / JavaSE

Are Scala's Case Classes A Failed Experiment?

2.11/5 (5 votes)
5 Jul 2008CPOL5 min read 2  
An article that discusses Scala's case classes

Introduction

A long time ago, Bertrand Meyer, the creator of Eiffel, made a very strong statement against the use of switch and case. He argued that these constructs were not object-oriented and made future maintenance difficult. And to make his point crystal clear, he decided that Eiffel would not support these statements.

This decision didn't go well with Eiffel users and Meyer eventually gave in and added switch and case to his language.

Meyer's initial position was certainly extreme but I think the foundation of his argument still stands, and I was reminded of this age old debate just today while attending Martin Odersky's Jazoon keynote, and more specifically, when he described Scala's "case classes".

Ever since I first read about case classes, I have been confused about their utility, and my puzzlement has not abated. Either I am missing something big or this feature is something that has been vastly overhyped and that should be avoided as much as possible.

Let's see why.

Example Used in Official Scala Documentation

Here is the example used in the official Scala documentation:

C#
def printTerm(term: Term) {
    term match {
        case Var(n) =>
            print(n)
        case Fun(x, b) =>
            print("^" + x + ".")
            printTerm(b)
        case App(f, v) =>
            Console.print("(")
            printTerm(f)
            print(" ")
            printTerm(v)
            print(")")
    }
}

This code is part of a hypothetical parser: it does different things depending on whether we just encountered a variable, a function or an application.

I have two strong objections to this kind of approach:

  • It ties this class very tightly to all the classes described in the various cases.
  • It makes evolution problematic.

I think the first argument is pretty obvious: whatever class this code belongs to, that class knows an awful lot about all the classes it is trying to use. This wouldn't be too much of a problem if the alternative didn't turn out to be free of this problem, as I'll show below.

The second claim is a bit more subtle, but is actually the most important.

Obviously, this parser is quite incomplete: what happens if I want support for import clauses?

First, I am probably going to create at least one new class Import and second, I need to remember to add it to this code. Worse: all this code does is print out the type of Term it just parsed. Obviously, we will be needing more logic to actually do the work, such as a function to verify that the Term is syntactically correct (say verify(), and also a function to generate the code or the abstract information related to that term (generateCode()). Each of these methods will also feature a match structure, which will need to be updated for each new class.

In summary: every time I add a new class, I need to remember to update three functions located somewhere in my code base.

Now we can see why Meyer dislikes switch statements so much...

The preferred way to solve these two problems is to make sure that each class encapsulates its own logic for all these operations:

C#
interface Term {
    void print();
    void verify();
    void generateCode();
}
public Var implements Term {
    public void print() {
        print(n)
    }
    public void verify() { ... }
    public void generateCode() { ... }
}
// same for Fun and Expr

Here is how we print a term with case classes (from the Scala documentation, again):

C#
Term t = ...;printTerm(t)

And with proper encapsulation:

C#
Term t = ...;t.print();

With the OO approach, the code shown at the very top is simply no longer needed! If you need to print the content of a Term, you just invoke print() on it. If you add a new class to your parser, all you need to implement is conveniently summarized in the Term interface: no need to go hunting through your entire code base for switch statements. Not only do we have clean encapsulation, but we also have locality.

When I exposed the crux of this argument to a few Scala experts, they overall agreed about the point and responded by pointing out that case classes were more useful as an alternative to the Visitor pattern.

Before turning our attention to this specific case, it's important to note that at this point, we are now looking at solving a niche problem. And quite a rare one, in my experience. If this is really the reason why case classes were invented, I am really left scratching my head about the decision to include such a big feature inside a language to solve such a small class of problems.

Anyway, let's take a look at the Visitor angle.

In a nutshell, Visitors are used to emulate multiple dispatch in languages that don't support it natively. In some way, you are extending virtual invocation to apply to the runtime type of parameters passed to your functions.

I'm not going into a deep discussion of the pros and cons of this approach, but I'll just observe that even in the few cases where I have had such a need, an approach similar to the encapsulation explained above usually turns out to be preferable to a switch statement. In other words, if you need to have different operations depending on whether your function is being invoked with parameters of types A or B is better implemented by hiding this particular detail inside the class handling this dispatch, and not in a global switch.

From my understanding of Scala's history, case classes were added in an attempt to support pattern matching, but after thinking about the consequences of the points I just gave, it's hard for me to see case classes as anything but a failure. Not only do they fail to capture the powerful pattern matching mechanisms that Prolog and Haskell have made popular, but they are actually a step backward from an OO standpoint, something that I know Martin feels very strongly about and that is a full part of Scala's mission statement.

To be fair, Scala navigates in very rough waters: finding a good compromise between running seamlessly on an existing OO platform and inventing a language that will not only accommodate fundamental functional programming paradigms but also leave the door open for advanced dynamic concepts such as open classes is no easy feat, and overall, Scala has done a very good job at finding the right balance. But unfortunately, not with case classes.

Or am I missing something?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)