A Review of Scala Case Classes (Video)
Goals
In this book I generally assume that you know the basics of the Scala programming language, but because case
classes are so important to functional programming in Scala it’s worth a quick review of what case classes are — the features they provide, and the benefits of those features.
Discussion
As opposed to a “regular” Scala class
, a case class
generates a lot of code for you, 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 each constructor parameter, because
case
class constructor parameters are publicval
fields by default. - (You won’t use
var
fields in this book, but if you did, mutator methods would also be generated for constructor parameters declared asvar
.) - An
unapply
method is generated, which makes it easy to usecase
classes inmatch
expressions. This is huge for Scala/FP. - As you’ll see in the next lesson, a
copy
method is generated. I never use this in Scala/OOP code, you’ll use it all the time in Scala/FP. equals
andhashCode
methods are generated, which lets you compare objects and easily use them as keys in maps (and sets).- A default
toString
method is generated, which is helpful for debugging.
A quick demo
To demonstrate how case classes work, here are a few examples that show each of these features and benefits in action.
No need for new
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 christina = Person("Christina", "niece")
christina: Person = Person(Christina,niece)
This is a nice convenience when writing Scala/OOP code, but it’s a terrific feature when writing Scala/FP code, as you’ll see throughout this book.
No mutator methods
Case class constructor parameters are val
by default, so an accessor method is generated for each parameter, but mutator methods are not generated:
scala> christina.name
res0: String = Christina
// can't mutate the `name` field
scala> christina.name = "Fred"
<console>:10: error: reassignment to val
christina.name = "Fred"
^
unapply
method
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> christina match { case Person(n, r) => println(n, r) }
(Christina,niece)
Conversely, if you try to use a regular Scala class in a match
expression like this, you’ll quickly see that it won’t compile.
You’ll see many more uses of case classes with match
expressions in this book because pattern macthing is a BIG feature of Scala/FP.
A class that defines an
unapply
method is called an extractor, andunapply
methods enable match/case expressions. (I write more on this later in this book.)
copy
method
A case
class also has a built-in copy
method that is extremely helpful when you need to clone an object and change one or more of the fields during the cloning process:
scala> case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
defined class BaseballTeam
scala> val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
cubs1908: BaseballTeam = BaseballTeam(Chicago Cubs,1908)
scala> val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
I refer to this process as “update as you copy,” and this is such a big Scala/FP feature that I cover it in depth in the next lesson.
equals
and hashCode
methods
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> christina == hannah
res1: Boolean = false
These methods also let you easily use your objects in collections like sets and maps.
toString
methods
Finally, case
classes also have a good default toString
method implementation, which at the very least is helpful when debugging code:
scala> christina
res0: Person = Person(Christina,niece)
Looking at the code generated by case classes
You can see the code that Scala case classes generate for you. To do this, first compile a simple case
class, then disassemble the resulting .class files with javap
.
For example, put this code in a file named Person.scala:
// note the `var` qualifiers
case class Person(var name: String, var age: Int)
Then compile it:
$ scalac Person.scala
scalac
creates two JVM class files, Person.class and Person$.class. Disassemble Person.class with this command:
$ javap Person
With a few comments that I added, this command 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(); # getter
public void name_$eq(java.lang.String); # setter
public int age(); # getter
public void age_$eq(int); # setter
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);
}
Next, 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 javap
shows, Scala generates a lot of source code when you declare a class as a case
class, including getter and setter methods, and the methods I mentioned: copy
, hashCode
, equals
, toString
, unapply
, apply
, and many more.
As you see, case classes have even more methods, including
tupled
,curry
,curried
, etc. I discuss these other methods in this book as the need arises.
Case class compared to a “regular” class
As a point of comparison, if you remove the keyword case
from that code — making it a “regular” Scala class
— then compile it and disassemble it, you’ll see that 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);
}
As you can see, that’s a BIG difference. The case
class results in 22 more methods than the “regular” class. In Scala/OOP those extra fields are a nice convenience, but as you’ll see in this book, these methods enable many essential FP features in Scala.
Summary
In this lesson I showed that the following methods are automatically created when you declare a class as a case class
:
apply
unapply
- accessor methods are created for each constructor parameter
copy
equals
andhashCode
toString
These built-in methods make case classes easier to use in a functional programming style.
What’s next
I thought it was worth this quick review of Scala case
classes because the next thing we’re going to do is dive into the case
class copy
method. Because you don’t mutate objects in FP, you need to do something else to create updated instances of objects when things change, and the way you do this in Scala/FP is with the copy
method.
See also
- Extractor objects in Scala
- Daniel Westheide has a good article on extractors
Update: All of my new videos are now on
LearnScala.dev