Mike Slinn

Case Classes

— Draft —

Published 2014-08-01. Last modified 2016-07-04.
Time to read: 5 minutes.

Scala case classes are terrific for creating domain models, pattern matching, and many other purposes. This lecture shows how to work with them.

The sample code for this lecture can be found in courseNotes/src/main/scala/CaseClasses.scala.

Case classes are great for building domain models and pattern matching. We won’t explore those uses in this lecture – for now, we’ll just take our first look at case classes. The Sealed Classes and Extractors lecture will discuss how to create case classes in a way that guarantees initialization logic is applied whenever a case class is constructed.

What is a Case Class?

Each case class is merely a regular class with some standard methods defined, plus an automatically defined companion object with standard methods defined. In addition, each of the class’s constructor parameters are also automatically defined as an immutable property, just as if the val keyword had prefixed each parameter. To define a case class, merely preface the class keyword with case. Let’s use the REPL to experiment with case classes.

Scala REPL
scala> case class Frog9a(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
defined class Frog9a 

This is equivalent to:

Scala code
case class Frog9b(val canSwim: Boolean, val numLegs: Int, val breathesAir: Boolean)

As with regular classes, if you want a property to be mutable you must preface it with the var keyword.

Scala code
case class Frog9c(canSwim: Boolean, var numLegs: Int, breathesAir: Boolean)

The case class’s automatically generated companion object defines the default method called apply so you don’t have to use the new keyword in order to create an instance. Notice that case classes also define a toString method which prints out the values of all the properties passed to the default constructor.

Scala REPL
scala> val frog9a = Frog9a(canSwim=true, 4, breathesAir=true)
frog9a: Frog9a = Frog9a(true,4,true)
scala>
val frog9b = Frog9b(canSwim=true, 4, breathesAir=true) frog9b: Frog9bb = Frog9b(true,4,true)
scala>
val frog9c = Frog9c(canSwim=true, 4, breathesAir=true) frog10: Frog9c = Frog9c(true,4,true)

Object Equality

Let’s revisit the Dog and Hog comparison we saw a few lectures ago, using case classes. Case classes automatically define a method called canEqual, which informs us if two objects can be compared. You must pass an instance of the class to be compared into canEqual, not a class name.

Scala REPL
scala> case class Dog(name: String)
defined class Dog
scala>
case class Hog(name: String) defined class Hog

Case classes automatically define a method called canEqual, which informs us if two objects can be compared. You must pass an instance of the class to be compared into canEqual, not a class name.

Scala REPL
scala> val dog1 = Dog("Fido")
dog1: Dog = Dog(Fido)
scala>
val dog2 = Dog("Fifi") dog2: Dog = Dog(Fifi)
scala>
dog1.canEqual(dog2) res4: Boolean = true
scala>
dog1==dog2 res1: Boolean = false

To clarify, an instance must be provided to compare, not a class name. Unfortunately, false is returned from this erroneous expression, instead of throwing an exception.

Scala REPL
scala> dog1.canEqual(Dog)
res2: Boolean = false 

Let’s say we want dogs with the same value of name to be equivalent, using a case-insensitive match. definition of Dog.

Scala code
case class Dog(name: String) {
  override def equals(that: Any): Boolean = canEqual(that) && name.toLowerCase==that.name.toLowerCase
  override def hashCode = name.toLowerCase.hashCode
}

Comparisons work as expected.

Scala REPL
scala> Dog("fido")==Dog("Fido")
res7: Boolean = true 

Now let’s define Hog.

Scala code
case class Hog(name: String) {
  override def equals(that: Any): Boolean = canEqual(that) && hashCode==that.hashCode
  override def hashCode = name.hashCode
}

Now let’s try to compare a dog with a hog:

Scala REPL
scala> val hog1 = Hog("Porky")
hog1: Hog = Hog(Porky)
scala>
dog1.canEqual(hog1) // Again, provide an instance to compare, not a class: dog1.canEqual(Hog) is wrong res7: Boolean = false
scala>
dog1==hog1 res9: Boolean = false

Arity 22 Limitation - Removed in Scala 2.11

The primary constructors for case classes can only accept a maximum of 22 properties. The implications of this are that database records with 23 or more fields cannot be persisted to a database. The arity 22 limitation of case classes has been removed in Scala 2.11.

Subclassing Traits and Classes into Case Classes

This is straightforward. Case classes can extend regular classes, abstract classes and traits, in exactly the same way that normal classes can extend other classes and traits. Here is an example of how to subclass a regular class as a case class.

Scala code
abstract class AbstractFrog(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) {
  override def toString = s"canSwim: $canSwim, numLegs=$numLegs, breathesAir=$breathesAir"
}
case class Frog10(canSwim: Boolean, numLegs: Int, breathesAir: Boolean) extends AbstractFrog(canSwim, numLegs, breathesAir)

Now lets create an instance:

Scala REPL
scala> val frog10 = Frog10(canSwim=true, 4, breathesAir=true)
frog10: Frog10 = canSwim: true, numLegs=4, breathesAir=true

Example from Play Framework

Play Framework defines a trait that looks something like the following. It does not matter how EssentialFilter is defined for the purposes of this explanation. I’ve used a bogus definition for simplicity.

Scala code
class EssentialFilter
trait HttpFilters { def filters: Seq[EssentialFilter] }

Now for the important bit. A subclass of HttpFilters is defined, called DefaultHttpFilters, which fulfills the HttpFilter trait’s contract by implementing the filters property as a val. We learned why this is allowable when we discussed the uniform access principle in the Setters, Getters and the Uniform Access Principle lecture. The collection of EssentialFilter was expressed as a varargs parameter, discussed in the Classes Part 2 lecture.

Scala code
class DefaultHttpFilters(val filters: EssentialFilter*) extends HttpFilters

A case class could have been used instead used (without the val prefix) like this:

Scala code
case class DefaultHttpFilters(filters: EssentialFilter*) extends HttpFilters

Do Not Subclass Case Classes

Case classes must not be subclassed. The compiler will prevent this to some extent, and you should not do so even when it lets you do so. Instead, express your type hierarchy using traits and normal classes, and consider case classes as the equivalent of Java’s final. Here is the error you get if you attempt to subclass a case class:

Scala REPL
scala> case class nope(override val canSwim: Boolean, override val numLegs: Int, override val breathesAir: Boolean) extends Frog10(canSwim, numLegs, breathesAir)
<console>:10: error: case class nope has case ancestor Frog10, but case-to-case inheritance is prohibited.
To overcome this limitation, use extractors to pattern match on non-leaf nodes. 

Operator Overloading Revisited

Lets rewrite the complex arithmetic example we saw in the lecture about Scala classes, and use case classes instead. Notice how the usage of the case class reads better because the new keyword is not required.

Scala code
case class Complex(re: Double, im: Double) {
  def + (another : Complex) = Complex(re + another.re, im + another.im)
def unary_- = Complex(-re, -im)
override def toString = s"${re} + ${im}i" }

Now let’s use this definition in some complex arithmetic operations:

Scala REPL
scala> Complex(2, 5) + Complex(1, -2)
res10: Complex = 3.0 + 3.0i
scala>
-Complex(1, -2) res11: Complex = -1.0 + 2.0i

Standard Methods

The example code for each method assumes the following definitions, which were defined earlier in this lecture.

Scala code
case class Frog9a(canSwim: Boolean, numLegs: Int, breathesAir: Boolean)
case class Frog9c(canSwim: Boolean, var numLegs: Int, breathesAir: Boolean)
val frog9a = Frog9a(canSwim=false, numLegs=4, breathesAir=true) val frog9c = Frog9c(canSwim=true, numLegs=4, breathesAir=true)

Companion Classes

Case classes are guaranteed to have at least the following methods defined:

  • copy - Copies a case class instance while allowing named properties to be modified
    Scala REPL
    scala> val frog9a2 = frog9a.copy(canSwim=true)
    frog9a2: Frog9a = Frog(true,4,true)
    scala>
    val frog9a3 = frog9a.copy(canSwim=true, numLegs=2) frog9a3: Frog9a = Frog9a(true,2,true)
  • canEqual - indicates if two objects can be compared.
    Scala REPL
    scala> Frog9a(true, 4, true).canEqual(Frog9c(true,4,true))
    res1: Boolean = false
    scala>
    frog9a2 canEqual frog9a3 res2: Boolean = true
  • equals - compare two objects. No error is given if they should not be compared - in that case, false is quietly returned. This is an alias for ==.
    Scala REPL
    scala> frog9a.equals(frog9c)
    res3: Boolean = false
    scala>
    frog9a equals frog9c res4: Boolean = false
    scala>
    frog9a == frog9c res5: Boolean = false
  • hashCode - A digest stores a hash of the data from an instance of the class into a single hash value. Hashcodes are the basis of equality tests - if two objects have the same hashCode then they are equal.
    Scala REPL
    scala> frog9a.hashCode
    res5: Int = 272580090 
  • productArity - a count of the number of constructor properties of the case class.
    Scala REPL
    scala> frog9a.productArity
    res6: Int = 3 
  • productElement - retrieve the untyped value of the nth case class constructor property.
    Scala REPL
    scala> frog9a.productElement(0)
    res7: Any = true
    scala>
    frog9a.productElement(1) res8: Any = 4
    scala>
    frog9a.productElement(2) res9: Any = true
  • productIterator - return an iterator of the case class constructor properties.
    Scala REPL
    scala> frog9a.productIterator.foreach(println) // prints property values
    false
    4
    true 
  • productPrefix - Simply the name of the case class.
    Scala REPL
    scala> frog9a.productPrefix
    res10: String = Frog9a 
  • toString - string representation of the case class instance. Often overridden.
    Scala REPL
    scala> frog9a.toString
    res11: String = Frog9a(true,4,true)
    scala>
    frog9a res12: Frog9a = Frog9a(true,4,true)

Companion Objects

Case class companion objects are guaranteed to have at least the following methods defined.

  • apply – the default method for the companion object, which is a factory that calls new to construct new instances.
  • toString – prints the name of the case class. You normally should use the instance’s toString method instead.
  • unapply – useful for pattern matching and value extraction. Unapply is devoted to this topic.

If you define a companion object for a case class, the methods and properties you define in the companion object will augment the default methods and properties in the automatically generated companion object.

Case Object

A case object is a singleton case class without a parameter list.

Scala REPL
scala> case object MyFrog extends AbstractFrog(true, 2, true)
defined object MyFrog 

Caution - Defining Types within Methods or Functions

It is not a good idea to define traits, classes or case classes inside methods or functions. The following shows an attempt to define a case class inside a method and then return an instance. The Scala compiler’s error message is long, convoluted, and completely misleading.

Scala REPL
scala> def foo = { case class Bar(a: String); Bar }
<console>:7: warning: inferred existential type Bar.type forSome { val Bar: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]}; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String @scala.annotation.unchecked.uncheckedVariance} }, which cannot be expressed by wildcards,  should be enabled
by making the implicit value scala.language.existentials visible.
This can be achieved by adding the import clause ’import scala.language.existentials’
or by setting the compiler option -language:existentials.
See the Scala docs for value scala.language.existentials for a discussion
why the feature should be explicitly enabled.
       def foo = { case class Bar(a: String); Bar }
                 ^
<console>:5: error: type mismatch;
 found   : Bar.type(in lazy value $result) where type Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton
 required: (some other)Bar.type(in lazy value $result) forSome { type (some other)Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String} }
  lazy val $result = foo
^ <console>:5: error: type mismatch; found : Bar.type(in value $result) where type Bar.type(in value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton required: Bar.type(in lazy value $result) forSome { type Bar.type(in lazy value $result) <: scala.runtime.AbstractFunction1[String,Bar] with Serializable{case def unapply(x$0: Bar): Option[String]} with Singleton; type Bar <: Product with Serializable{val a: String; def copy(a: String): Bar; def copy$default$1: String} } lazy val $result = foo

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.