Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Introduction to Scala – case classes, matching, symbols, enumerations and sealed classes

2007, howto, scala

In the previous post, we covered using the compiler and packages. This time, we'll be looking at one of the features that makes Scala stand out above the rest; case classes and matching.

The idea behind case classes is to support data encapsulated as an object, but to also support structural matching of the data as well. A case class is defined in much the same way as an ordinary class, except that the class definition is prefixed with case:

scala> case class Card(value:Int) {}
defined class Card

Right, so what's different with using this without the case? The case class supports an automatic equals implementation:

scala> class NonCaseCard(value:Int) {}
defined class NonCaseCard
scala> new NonCaseCard(3) == new NonCaseCard(3)
res1: Boolean = false
scala> new Card(3) == new Card(3)
res2: Boolean = true

Here, the == is invoking equals under the covers; so you're getting a value comparison, not a reference comparison. (If you want a reference comparison, you can call eq or its corresponding negation ne.) Note that this equals comparison is implemented properly; in other words, it's a proper equivalence relation; there's no danger of getting it wrong.

As well as being able to compare instances, it's also possible to match against existing patterns. Scala has a keyword match which permits comparison of an expression against a number of different types; think of it as a switch on steroids. The general format of the match looks like this:

scala> expr match {
     |   case x:Int => Console.println("X is an Int")
     |   case y:String => Console.println("Y is a String")
     |   case _ => Console.println("It's something else")
     | }

Depending what the type of expr is, a different result will be printed. However, we're not limited to types; we can supply values, too:

scala> int match {
     |   case 1 => "One"
     |   case 2 => "Two"
     |   case 3 => "Three"
     |   case 4 => "Many"
     | }

In this case, there's no default (_), so if the int value isn't part of the known set, you'll get a MatchError (a subclass of RuntimeException) at runtime.

Back to case classes. We can match case classes as you might expect, but in addition we can automatically decompose them, too:

scala> def name(c:Card) = c match {
     |   case Card(1) => "Ace"
     |   case Card(11) => "Jack" // Or Knave, depending on preference
     |   case Card(12) => "Queen"
     |   case Card(13) => "King"
     |   case Card(x:Int) => x.toString()
     | }
name: (Card)java.lang.String
scala&t; val rimmer = name(new Card(1))
rimmer: java.lang.String = Ace

Note that we don't supply a default which will match any type here; we're only interested in printing out names of Cards. Because it's a case class, Scala knows how to match it against the instance that is being pulled in, instead of having to separately pull out the part and investigate it ourselves. (In other languages like Java, you'd have to do something like switch(c.value); but that doesn't let you do much if you have several values that you want to pass.)

It turns out that Scala's List is also a case class, and as such, we can use that in matching operations as well. This is quite useful if you want to match against multiple elements:

scala> def size(x:List[Any]) = x match {
     |   case List() => "None"
     |   case List(_) => "One"
     |   case List(_,_) => "Two"
     |   case List(_,_,_) => "Three"
     |   case _:List[Any] => "Many"
     | }
size: (List[Any])java.lang.String
scala> val three = size(List(1,2,3))
three: java.lang.String = Three
scala> val many = size(List(1,2,3,4))
many: java.lang.String = Many

This allows us to inspect details several layers deep in list structures. Here, I'm using the "don't care" pattern _, but I could equally have pulled out the values like the Card example.

So, what about adding suit values to the card as well? We can do this in several ways; using a Symbols, using an Enumeration or plain old subclassing. Let's take a look at each:

scala> case class Card(value:Int, suit:Symbol) {}
defined class Card
scala> val h3 = new Card(3,'Hearts)
h3: Card = Card(3,'Hearts)
scala> val d2 = new Card(2,Symbol("Diamonds"))
d2: Card = Card(2,'Diamonds) 

We can instantiate a Symbol either using the explicit constructor, or using the single-quote shorthand. As you can see from the output, they have exactly the same effect. So what's a Symbol? Well, it's like a dynamically created instance of a String; all Symbols with the same value are equivalent. We can use this to update our output routine:

scala> def print(c:Card) = c match {
     |   case Card(1,'Spades) => "Ace of Spades (taxed)"
     |   case Card(1,suit:Symbol) => "Ace of " +
     |   case Card(11,suit:Symbol) => "Jack of " + // Or Knave, depending on preference
     |   case Card(12,suit:Symbol) => "Queen of " +
     |   case Card(13,suit:Symbol) => "King of " +
     |   case Card(x:Int,suit:Symbol) => x.toString() + " of " +
     | }
scala> val wonderland = print(new Card(12,'Hearts))
wonderland: java.lang.String = Queen of Hearts

The Symbol is a short, convenient way of passing in data that's matchable and also unique. It's quite often used with tuples in languages like Erlang or Lisp as a poor man's data type or structured record. Thus, tuples such as ('Card,'Hearts, 1) and ('Card, 'Spades, 2) might be ways of representing structured data in such languages. One advantage of using data structures like this is that they're easy to stream (they're all tuples) and the Symbols remain the same after streaming across a network.

The main problem with Symbols is also their strength; they can take any value that you want. That works great for message passing systems; you're not restricted by the types that you send around the network, and as long as you handle the 'unknown' case appropriately, you have an extensible protocol.

That doesn't work so well for Cards, which are limited to one of the standard suits. Granted, we could make use of the class' initialiser and throw an error when the Symbol wasn't what we wanted; but that wouldn't be a type-safe way of dealing with it. Another way to achieve the same goal, we can use an Enumeration to define our suit types:

scala> object Suit extends Enumeration {
     |  val Club, Heart, Diamond, Spade = Value
     | }
defined module Suit
scala> case class Card(value:Int, suit:Suit.Value) {}
defined class Card
scala> val h3 = new Card(3,Suit.Heart())
h3: Card = Card(3,Suit(1))

It might not display well, but it does show what's happening. In fact, we can create cards either with their textual suit names or the enumeration values:

scala> val h3 = new Card(3,Suit(1))
h3: Card = Card(3,Suit(1))

Whether having numeric values for suits is a good idea or not is an implementation detail. Card games like bridge place an ordering on the suits, so it may work well for some uses. In addition, there are some standard features that Enumerations support such as map and foreach:

scala> val suits = Suit map (value => value toString) toList
suits: List[java.lang.String] = List(Suit(0), Suit(1), Suit(2), Suit(3))

The last way of representing this would be to create direct instances of a class to represent the suits. This might be more flexible depending on your requirements; in this case, we can create an abstract class Suit and subclasses thereof. In order to prevent other new Suits being created, we can seal the class; which prevents subclassing outside of its current scope. To keep this nice and tidy, we define it in an object called Cards; in effect, defining static-like fields:

scala> object Cards {
     |   sealed abstract class Suit
     |   case class Club extends Suit
     |   case class Diamond extends Suit
     |   case class Heart extends Suit
     |   case class Spade extends Suit
     | }
defined module Card
scala> case class Card(value:Int, suit:Cards.Suit)
defined class Card
scala&t; val h3 = Card(3,Cards.Heart())
h3: Card = Card(3,Heart())

We finally have a version of Card in which we can guarantee at compile time that the value is valid. Note that due to the use of the sealed parameter in the Suit definition, we cannot have any further subclasses of Suit created; we know they're one of the four values. As the subclasses are case classes, we can use them in match expressions, like before. In a real implementation, we'd probably want to override the toString method. Note also that if you don't supply any members (as here) then the {} is optional; so I've ommitted them from this last example.

Lastly, if we wanted to make things easier, we could have done import Cards._, which would mean we could then define our class using suit:Suit and the instances with Card(3,Heart) which would work just as well. (If you're typing all these into the interpreter and get a "reference ambiguous" error then try starting a new session; it's because we've been reusing names throughout this set of examples.)

In this post, we looked at defining case classes and using them in match expressions, and for an added bonus, looked at symbols, enumerations and sealed classes. Next up: traits.