This is ericpony's blog

Wednesday, February 12, 2014

Scala: Case Classes

In essential, case classes can be seen as plain and immutable data-holding objects that should exclusively depend on their constructor arguments.
Case classes differ with ordinary classes in that they
  • automatically define hashCode, equals, and toString methods
  • automatically define getter methods for the constructor arguments
This functional concept allows us to
  • use a compact initialization syntax, e.g., Node(Leaf(2), EmptyLeaf)
  • decompose them using pattern matching
  • have equality comparisons implicitly defined
Case classes are by default immutable data structures, as they implicitly use val constructor parameters. They are suitable to works with hash maps or sets, since they have a valid, stable hashCode. One can override the default behavior by prepending each constructor argument with var instead of val for case classes. In this way, you get the same getter/setter generation just like ordinary classes. However, making case classes mutable causes their equal and hashCode methods to be time variant. If an object performs stateful computations on the inside or exhibits other kinds of complex behaviour, it should instantiate an ordinary class instead of a case class. [1]

A case class automatically creates a companion object with the same name as the class, which contains apply and unapply methods. The apply method enables constructing instances without prepending with new. The unapply extractor method enables the pattern matching that mentioned in Note I. The compiler optimizes the speed of pattern matching for case classes. [2]

In combination with inheritance, case classes are often used to mirror algebraic data types. A very simple example of such types are trees. A binary tree, for instance, can be implemented like this:
sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree
That enable us to do the following:
// DSL-like assignment:
val treeA = Node(EmptyLeaf, Leaf(5))
val treeB = Node(Node(Leaf(2), Leaf(3)), Leaf(5))
// modification through cloning:
val treeC = treeA.copy(left = treeB.left)
// Pretty printing
println("Tree A: " + treeA)
println("Tree B: " + treeB)
println("Tree C: " + treeC)
// Comparison
println("Tree A == Tree B: %s" format (treeA == treeB).toString)
println("Tree B == Tree C: %s" format (treeB == treeC).toString)
// Pattern matching
treeA match {
  case Node(EmptyLeaf, right) => println("Can be reduced to " + right)
  case Node(left, EmptyLeaf) => println("Can be reduced to " + left)
  case _ => println(treeA + " cannot be reduced")
}
Note that trees construct and deconstruct (through pattern match) with the same syntax, which is also exactly how they are printed (minus spaces).

Technically, one may treat case classes as instances of Product and thus inherit these methods:
def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]
where the productArity returns the number of class parameters, productElement(i) returns the ith parameter, and productIterator allows iterating through them.

References

1. Case classes are cool
2. Case Classes and Extractors, p.15

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...