Scala case classes are regular classes with some additional features. The compiler adds convenient and useful methods as well as the apply method in the companion object to case classes which makes such classes handier to deal with data. We shall see how we can use pattern matching after deconstructing case classes in practical examples; one such example is implementing a token executor in a few lines of code.

Defining the case class

Let’s start off by creating our first case class. In this case class, we shall model a person. Our person will have two fields; name and surname.

case class Person(name: String, surname: String)

It can be seen that syntactically, the difference between a normal class and a case class is the prefix case.

Whenever the scala compiler encounters a case class during compilation, several functionalities are added by default such as the factory method, equality and toString methods and support for deconstruction.

The factory method

By default, scala creates a companion object with an apply method which is used to instantiate the class without the use of the new keyword. This is known as the factory method.

object Person {

def apply(name: String, surname: String): Person = new Person(name, surname)

}

Because of this factory method, we can instantiate a Person named Joe Bloggs without the need to use the new keyword.

Person(“Joe”, “Bloggs”)

In scala, classes can be accompanied by a companion object. As scala does not have the static keyword, “static” fields and methods reside in a class’ companion object. The apply method resides in the companion object. It can be seen that the apply method is essentially a “static” method which calls the class’ constructor; just syntactic sugar :)

Equality and toString Methods

One common pitfall of object-oriented developers during early days in development is when performing object equality on classes with the same values such as two objects with both name and surname Joe Bloggs and result in them being classified as not equal. This usually happens because of one of two reasons:

performing identity check instead of equality ( eq method instead of == )

method instead of ) not overriding the equals method

When using case classes, (thankfully), the compiler eliminates the need to manually implement the boilerplate equals , hashCode and toString methods by implementing these methods on the parameters of the case class. In the example of the case class Person, these are implemented on the fields name: String and surname: String .

Thus, when performing == on two instances of case classes with the same parameter values;

Person("Joe", "Bloggs") == Person("Joe", "Bloggs")

the result would be true.

Lastly, calling

Person("Joe", "Bloggs")

would print

Person(Joe,Bloggs)

to the console. This is achieved without implement the boilerplate toString method.

Immutability

By default, parameters in regular and case classes are val that is they are immutable. Thus, when instantiating a case class with an object, it can be seen that the instance is frozen; the contents of it cannot be changed.

Having immutable instances with automatically generated equality methods make case classes handy to hold data. In fact, sometimes case classes are informally known as data containers. These data container classes, apart from making the data easy to access and compare, makes dealing with data from concurrent contexts less painful when variables are declared immutable.

Deconstruction

One handful scala feature for case classes is deconstruction. When deconstructing case classes, the values of the parameter variable of a case class are deconstructed into their constituent parts. For instance, we can deconstruct the case class Person to extract both the firstName and lastName in one go.

val p = Person("Joe", "Bloggs")

val Person(pName, pSurname) = p

print(s”p’s name is $pName and p’s last name is $pSurname”)

In line 2, we obtain the firstName and lastName of joe by means of deconstruction. Now we can make use of the variables pName and pSurname normally and print the name and surname as is done in line 3.

Pattern matching on case classes

We have seen how we can deconstruct a case class. Deconstruction enables us to perform pattern matching on case classes. Using deconstruction, we can perform pattern matching on the deconstructed values of a case class. Let’s see this in action. Let’s say we want to create a function which returns true if the person’s first name is Joe.

We first call match on the Person instance, deconstruct the person and match on Joe as the first name.

def isPersonJoe(p: Person): Boolean = person match {

case Person(“Joe”, _) => true

case _ => false

}

On line 2, we deconstruct the person and return true if the deconstructed firstName matches the text Joe. In scala, the _ wildcard operator in case statements is similar to the default keyword in switch statements; this matches on any previously unmatched case. On line 3, we are returning false on all other persons who do not match the case on line 2.

Pattern matching on strings

Pattern matching comes particularly handy when a snippet of code employs several nested (and frustratingly illegible) if else statements, or a switch statement. In contrast to the verbosity of having several levels of nesting of these if else statements, pattern matching statements are explicit and concise.

Let’s create a snippet which parses command line argument flags using the if else pattern.

def parseArgument(arg: String) = {

if (arg == "-h" || arg == "--help") {

printHelp

} else {

if (arg == "-v" || arg == "--version") {

printVersion

} else {

printUnknownArg arg

}

}

}

One could get lost in the nesting of conditional statements the code which could result in bugs. Let’s see how we can write this concisely using pattern matching.

def parseArgument(arg: String) = arg match {

case "-h” | “ — help” => printHelp

case “-v” | “ — version” => printVersion

case unknownArg => printUnknownArg unknownArg

}

Using pattern matching statement, the verbosity of code can be drastically reduced resulting in code which is concise and easy to read.

Pattern matching on lists

Pattern matching can also be applied to the collections, collections use case classes in their structures. For instance, the immutable List is a Linked List implemented using case classes at each position in the list. Recall from functional programming 101, the :: (cons) operator adds a head element to a pre-existing list. In scala, :: is a case class which appends a head to an existing List. As the empty list is an object Nil , it can be seen that the list of the first five integers can be expressed as

1 :: 2 :: 3 :: 4 :: 5 :: Nil

Reversing a list can be achieved using recursion and the use of pattern matching. We would first need to deconstruct the list to obtain the head and the tail of the list, then reverse tail and append the head to it (this is quite expensive but is ideal for illustration).

def reverse[T](list: List[T]): List[T] = list match {

case Nil => Nil

case head :: tail => reverse(tail) ::: List(head)

}

Reversing a list using pattern matching can be seen as explicit and concise.

Bonus: Writing a token executor using pattern matching

Compilers are programs which perform analysis, tokenise and execute tokens from source code. In this section, we shall see how we can create the execution context from tokens using pattern matching.

In this simple example, we shall support integer values, and the addition and multiplication operators. Let’s first create the tokens.

sealed trait Expression case class Add(left: Expression, right: Expression) extends Expression case class Mult(left: Expression, right: Expression) extends Expression case class Number(value: Int) extends Expression

Now let’s perform the heavy lifting, let’s write the executors for these expressions.

def eval(expr: Expression): Int = expr match {

case Number(value) => value

case Add(left, right) => eval(left) + eval(right)

case Mult(left, right) => eval(left) * eval(right)

}

Although this might have sounded to be rather complex, using pattern matching, we implemented the semantics of the expressions in one location in the code.

Conclusion

In this post, we saw how classes provide handy extensions to normal classes for when we are more interested in the state rather than the behaviour of classes. Through the automatic implementation of equals, hashCode and toString on the parameters of case classes, boilerplate code is drastically reduced. Using pattern matching after deconstruction, we saw how we can write concise code as an alternative to if statements.