4.14. Generating Boilerplate Code with Case Classes

Problem

You’re working with match expressions, actors, or other situations where you want to use the case class syntax to generate boilerplate code, including accessor and mutator methods, along with apply, unapply, toString, equals, and hashCode methods, and more.

Solution

Define your class as a case class, defining any parameters it needs in its constructor:

// name and relation are 'val' by default
case class Person(name: String, relation: String)

Defining a class as a case class results in a lot of boilerplate code being generated, with the following benefits:

  • An apply method is generated, so you don’t need to use the new keyword to create a new instance of the class.

  • Accessor methods are generated for the constructor parameters because case class constructor parameters are val by default. Mutator methods are also generated for parameters declared as var.

  • A good, default toString method is generated.

  • An unapply method is generated, making it easy to use case classes in match expressions.

  • equals and hashCode methods are generated.

  • A copy method is generated.

When you define a class as a case class, you don’t have to use the new keyword to create a new instance:

scala> case class Person(name: String, relation: String)
defined class Person

// "new" not needed before Person
scala> val emily = Person("Emily", "niece")
emily: Person = Person(Emily,niece)

Case class constructor parameters are val by default, so accessor methods are generated for the parameters, but mutator methods are not generated:

scala> emily.name
res0: String = Emily

scala> emily.name = "Fred"
<console>:10: error: reassignment to val
       emily.name = "Fred"
                  ^

By defining a case class constructor parameter as a var, both accessor and mutator methods are generated:

scala> case class Company (var name: String)
defined class Company

scala> val c = Company("Mat-Su Valley Programming")
c: Company = Company(Mat-Su Valley Programming)

scala> c.name
res0: String = Mat-Su Valley Programming

scala> c.name = "Valley Programming"
c.name: String = Valley Programming

Case classes also have a good default toString method implementation:

scala> emily
res0: Person = Person(Emily,niece)

Because an unapply method is automatically created for a case class, it works well when you need to extract information in match expressions, as shown here:

scala> emily match { case Person(n, r) => println(n, r) }
(Emily,niece)

Case classes also have generated equals and hashCode methods, so instances can be compared:

scala> val hannah = Person("Hannah", "niece")
hannah: Person = Person(Hannah,niece)

scala> emily == hannah
res1: Boolean = false

A case class even creates a copy method that is helpful when you need to clone an object, and change some of the fields during the process:

scala> case class Employee(name: String, loc: String, role: String)
defined class Employee

scala> val fred = Employee("Fred", "Anchorage", "Salesman")
fred: Employee = Employee(Fred,Anchorage,Salesman)

scala> val joe = fred.copy(name="Joe", role="Mechanic")
joe: Employee = Employee(Joe,Anchorage,Mechanic)

Discussion

Case classes are primarily intended to create “immutable records” that you can easily use in pattern-matching expressions. Indeed, pure FP developers look at case classes as being similar to immutable records found in ML, Haskell, and other languages.

Perhaps as a result of this, case class constructor parameters are val by default. As a reviewer of this book with an FP background wrote, “Case classes allow var fields, but then you are subverting their very purpose.”

Generated code

As shown in the Solution, when you create a case class, Scala generates a wealth of code for your class. To see the code that’s generated for you, first compile a simple case class, then disassemble it with javap. For example, put this code in a file named Person.scala:

case class Person(var name: String, var age: Int)

Then compile the file:

$ scalac Person.scala

This creates two class files, Person.class and Person$.class. Disassemble Person.class with this command:

$  javap Person

This results in the following output, which is the public signature of the class:

Compiled from "Person.scala"
public class Person extends java.lang.Object implements scala.ScalaObject,scala.Product,scala.Serializable{
  public static final scala.Function1 tupled();
  public static final scala.Function1 curry();
  public static final scala.Function1 curried();
  public scala.collection.Iterator productIterator();
  public scala.collection.Iterator productElements();
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
  public Person copy(java.lang.String, int);
  public int copy$default$2();
  public java.lang.String copy$default$1();
  public int hashCode();
  public java.lang.String toString();
  public boolean equals(java.lang.Object);
  public java.lang.String productPrefix();
  public int productArity();
  public java.lang.Object productElement(int);
  public boolean canEqual(java.lang.Object);
  public Person(java.lang.String, int);
}

Then disassemble Person$.class:

$  javap Person$

Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction2 implements scala.ScalaObject,scala.Serializable{
  public static final Person$ MODULE$;
  public static {};
  public final java.lang.String toString();
  public scala.Option unapply(Person);
  public Person apply(java.lang.String, int);
  public java.lang.Object readResolve();
  public java.lang.Object apply(java.lang.Object, java.lang.Object);
}

As you can see, Scala generates a lot of source code when you declare a class as a case class.

As a point of comparison, if you remove the keyword case from that code (making it a “regular” class), compile it, and then disassemble it, Scala only generates the following code:

public class Person extends java.lang.Object{
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
  public Person(java.lang.String, int);
}

That’s a big difference. The case class results in 22 more methods than the “regular” class. If you need the functionality, this is a good thing. However, if you don’t need all this additional functionality, consider using a “regular” class declaration instead. For instance, if you just want to be able to create new instances of a class without the new keyword, like this:

val p = Person("Alex")

create an apply method in the companion object of a “regular” class, as described in Recipe 6.8. Remember, there isn’t anything in a case class you can’t code for yourself.

See Also

  • Recipe 4.3, shows how to write additional apply methods so a case class can appear to have multiple constructors.

  • A discussion of extractors on the official Scala website.

Get Scala Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.