What are the disadvantages to declaring Scala case classes?

Graham Lea picture Graham Lea · Jan 11, 2011 · Viewed 21.2k times · Source

If you're writing code that's using lots of beautiful, immutable data structures, case classes appear to be a godsend, giving you all of the following for free with just one keyword:

  • Everything immutable by default
  • Getters automatically defined
  • Decent toString() implementation
  • Compliant equals() and hashCode()
  • Companion object with unapply() method for matching

But what are the disadvantages of defining an immutable data structure as a case class?

What restrictions does it place on the class or its clients?

Are there situations where you should prefer a non-case class?

Answer

Kevin Wright picture Kevin Wright · Jan 11, 2011

First the good bits:

Everything immutable by default

Yes, and can even be overridden (using var) if you need it

Getters automatically defined

Possible in any class by prefixing params with val

Decent toString() implementation

Yes, very useful, but doable by hand on any class if necessary

Compliant equals() and hashCode()

Combined with easy pattern-matching, this is the main reason that people use case classes

Companion object with unapply() method for matching

Also possible to do by hand on any class by using extractors

This list should also include the uber-powerful copy method, one of the best things to come to Scala 2.8


Then the bad, there are only a handful of real restrictions with case classes:

You can't define apply in the companion object using the same signature as the compiler-generated method

In practice though, this is rarely a problem. Changing behaviour of the generated apply method is guaranteed to surprise users and should be strongly discouraged, the only justification for doing so is to validate input parameters - a task best done in the main constructor body (which also makes the validation available when using copy)

You can't subclass

True, though it's still possible for a case class to itself be a descendant. One common pattern is to build up a class hierarchy of traits, using case classes as the leaf nodes of the tree.

It's also worth noting the sealed modifier. Any subclass of a trait with this modifier must be declared in the same file. When pattern-matching against instances of the trait, the compiler can then warn you if you haven't checked for all possible concrete subclasses. When combined with case classes this can offer you a very high level level of confidence in your code if it compiles without warning.

As a subclass of Product, case classes can't have more than 22 parameters

No real workaround, except to stop abusing classes with this many params :)

Also...

One other restriction sometimes noted is that Scala doesn't (currently) support lazy params (like lazy vals, but as parameters). The workaround to this is to use a by-name param and assign it to a lazy val in the constructor. Unfortunately, by-name params don't mix with pattern matching, which prevents the technique being used with case classes as it breaks the compiler-generated extractor.

This is relevant if you want to implement highly-functional lazy data structures, and will hopefully be resolved with the addition of lazy params to a future release of Scala.