In addition to regular classes, Scala also provides case classes for the purpose of pattern matching. They offer a few benefits that standard classes do not.
Getting Personal
Defining a case class is stupid easy:
case class Person(first: String, last: String)
Person
has two fields, first
and last
, both of type string
. Notice that the parameters don’t need a val
keyword.
Defining a Person
variable is even easier than a standard class because you can omit the new
keyword:
val me = Person("Brad", "Collins")
Pattern matching on case class instances is pretty straightforward, too:
def greet(p : Person) = p match { case Person("Brad", "Collins") => "It's me!" case Person("Brad", _) => "Nice name" case Person(_, "Collins") => "Greetings, kinfolk" case _ => "Hello, stranger" } val me = Person("Brad", "Collins") val kin = Person("Shad", "Collins") val namesake = Person("Brad", "Rollins") val stranger = Person("Ezra", "Shemiah") val meGreeting = greet(me) // meGreeting: String = It's me! val kinGreeting = greet(kin) // kinGreeting: String = Greetings, kinfolk val namesakeGreeting = greet(namesake) // namesakeGreeting: String = Nice name val strangerGreeting = greet(stranger) // strangerGreeting: String = Hello, stranger
To match on a Person
object with specific first
and last
fields, specify both of them, as in the first match case above. To match on just one field, either first
or last
, specify the field to match, and use the wildcard pattern, the underscore (_
), for the field you don’t care about, as in the second and third match cases above. If Person
had more than two fields, you could match any subset of them by specifying the field values you need to be exact and using the wildcard pattern for the fields you don’t.
One other thing to note here: Order matters. What if you had defined greet
this way?
def greet(p : Person) = p match { case Person("Brad", _) => "Nice name" case Person(_, "Collins") => "Greetings, kinfolk" case Person("Brad", "Collins") => "It's me!" // OOPS! case _ => "Hello, stranger" }
The third case (line 4 above) would never match because Person("Brad", "Collins")
would always match the first case. So, pay attention out there.
Personal Improvement
As with regular classes, we can add member functions and properties to the case class itself and also put some things in a companion object:
case class Person(first: String, last: String) { def swap = Person(last, first) } object Person { val anonymous = Person("", "") } val me = Person("Brad", "Collins") // me: Person = Person(Brad,Collins) val swapped = me.swap // swapped: Person = Person(Collins,Brad) val johnDoe = Person.anonymous // johnDoe: Person = Person(,)
This example also demonstrates one of the extras you get with class classes: A toString()
implementation. With a regular class, if you want toString()
to show you something more descriptive than a type name, you have to overload toString()
yourself.
Case classes also throw in implementations of equals(o)
and hashCode()
for no charge:
val me = Person("Brad", "Collins") val swapped = me.swap val myself = Person("Brad", "Collins") val iAm = me == myself // iAm: Boolean = true val iAint = me == swapped // iAint: Boolean = false val meHash = me.hashCode() // meHash: Int = 777586888 val myselfHash = myself.hashCode() // myselfHash: Int = 777586888 val swappedHash = swapped.hashCode() // swappedHash: Int = 1042444723
Notice how me
and myself
are equal though they are different object instances, and they also have the same hash code.
Finally, if you have a case class instance and need a new instance with, say, only one or two field values that are different, case classes throw in the copy()
to do just that so that you don’t have to set every field value explicitly:
val me = Person("Brad", "Collins") val kin = me.copy(first = "Shad") // kin: Person = Person(Shad,Collins) val namesake = me.copy(last = "Rollins") // namesake: Person = Person(Brad,Rollins)
Personable Companions
The way case classes perform some of their magic is that Scala defines a companion object for each case class behind the scenes.
First, why don’t you need the new
keyword when instantiating case classes? Because the companion object has an apply()
method that takes the parameters defined in the case class constructor:
case class Person(first: String, last: String) // Notional representation of what the // compiler provides: // // object Person { // def apply(first: String, last: String) = // new Person(first, last) // // def unapply(p: Person): Option[String, String] = // Some(p.first, p.last) // } val me = Person("Brad", "Collins") // Actually calls ... // Person.apply("Brad", "Collins")
That unapply()
method is how Scala destructures case classes when pattern matching. When you write this:
p match { case Person(f,l) => ... }
… Scala uses Person.unapply(p)
to populate the f
and l
variables.
One final note: When we added the anonymous
field to the Person
companion object above, the compiler was nice enough to merge that with the one it generated for us instead of overwriting the generated one with ours.
Watch out if you attempt to type that out into the REPL, you do overwrite the compiler-generated Person
companion object. (Hint: Use the REPL’s :paste
function to get around that.)