Categories
Tech

Scala Saturday – Pattern Matching, Part 2: Case Classes

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.)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.