This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 4.14, “How to generate boilerplate code with Scala case
classes.”
Problem
You’re working with Scala 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 thenew
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 asvar
. - A good, default
toString
method is generated. - An
unapply
method is generated, making it easy to use case classes inmatch
expressions. equals
andhashCode
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 cloning 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, “Creating Object Instances Without Using the new Keyword”. Remember, there isn’t anything in a case class you can’t code for yourself.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- Recipe 4.3, “Defining Auxiliary Constructors in Scala Classes” 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