Wednesday, October 14, 2015

Scala case Class Introspection

I like the case class and use it quite liberally when I want to define an entity that is a collection of value types and want automatic toString and getter methods for those data elements. While the same can be done using Tuples, case classes are better because you can access the data elements or members by their names leading to readable code. So how does that happen in case classes? What happens when you define a case class? Lets answer those questions by defining a simple case class and looking at the compiled bytecode.

c:\tmp>type CaseExample.scala
case class CaseExample(name: String, value: Int)
c:\tmp>scalac CaseExample.scala
c:\tmp>scalap CaseExample
case class CaseExample(name : scala.Predef.String, value : scala.Int) extends scala.AnyRef with scala.Product with scala.Serializable {
val name : scala.Predef.String = { /* compiled code */ }
val value : scala.Int = { /* compiled code */ }
def copy(name : scala.Predef.String, value : scala.Int) : CaseExample = { /* compiled code */ }
override def productPrefix : java.lang.String = { /* compiled code */ }
def productArity : scala.Int = { /* compiled code */ }
def productElement(x$1 : scala.Int) : scala.Any = { /* compiled code */ }
override def productIterator : scala.collection.Iterator[scala.Any] = { /* compiled code */ }
def canEqual(x$1 : scala.Any) : scala.Boolean = { /* compiled code */ }
override def hashCode() : scala.Int = { /* compiled code */ }
override def toString() : java.lang.String = { /* compiled code */ }
override def equals(x$1 : scala.Any) : scala.Boolean = { /* compiled code */ }
}
object CaseExample extends scala.runtime.AbstractFunction2[scala.Predef.String, scala.Int, CaseExample] with scala.Serializable {
def this() = { /* compiled code */ }
final override def toString() : java.lang.String = { /* compiled code */ }
def apply(name : scala.Predef.String, value : scala.Int) : CaseExample = { /* compiled code */ }
def unapply(x$0 : CaseExample) : scala.Option[scala.Tuple2[scala.Predef.String, scala.Int]] = { /* compiled code */ }
}
c:\tmp>scalap CaseExample$
package CaseExample$;
final class CaseExample$ extends scala.runtime.AbstractFunction2 with scala.Serializable {
def this(): scala.Unit;
def apply(scala.Any, scala.Any): scala.Any;
def readResolve(): scala.Any;
def unapply(CaseExample): scala.Option;
def apply(java.lang.String, scala.Int): CaseExample;
def toString(): java.lang.String;
}
object CaseExample$ {
final val MODULE$: CaseExample$;
}

As you can see above, when we define a case class, a couple of things happened. First, the class was defined with the traits scala.Product and scala.Serialiable. Second, the class arguments were automatically defined as vals in the class, along with the "getter" methods. Third, some additional methods were added to the class - viz. copy, productPrefix, productArity, productElement, productIterator and canEqual. And fourth, a companion object was also defined.

So what is this Product trait? you can see in the Scala API, that the trait is inherited by all Tuple and case classes. And that's where the additional methods came from. Some of the methods were implementations of abstract methods in the Product trait while the others were overrides.
Here's an example of how you can impact the case class' toString by overriding the productPrefix method from Product.


scala> case class C(i: Int) {override def productPrefix = "MYCLASS" }
defined class C
scala> val c = C(4)
c: C = MYCLASS(4)

The other big benefit of the case class is that it extends scala.Serializable. If your case class is made up of value types (e.g. Int or String), you don't need to do anymore work to read/write instances of case classes from/to files, streams, etc.

Thus case classes give you two freebies - toString and serializability.

Beware - case Classes Cannot Be Inherited

However a gotcha of case classes is that you cannot inherit them. So there is a likelyhood of code reptition if the common code is repeated in each related case class. One way to avoid code repitition is to use traits as a base for the common methods and parameters. Here's an example:

case class Dog(name: String, breed: String)
case class Cat(name: String, breed: String)
case class Horse(name: String, breed: String)
trait AnimalLike {
def name: String
def breed: String
}
case class Dog(val name: String, val breed: String) extends AnimalLike


All class constructor parameters and any other common class variables and methods that need to be part of all case classes need to (or can be) be defined in the trait. The variables should be defined as abstract as shown above.


No comments:

Post a Comment