Scala pattern matching: apply the unapply

Linas Medžiūnas
Wix Engineering
Published in
7 min readJan 31, 2019

In my previous blog post, we delved under the hood of Scala for-comprehension. This time, we’ll take a similar approach and will become familiar with another great feature of this powerful programming language — pattern matching. Again, we’ll be doing it in a series of small steps, by writing some short snippets of code.

We begin by declaring a very simple case class that we will soon dissect:

case class FullName(first: String, last: String)

Among many other useful features of case class (such as structural equals, hashCode, copy and toString), Scala compiler provides support for the following code, which constructs and then deconstructs an instance of this case class:

val me = FullName("Linas", "Medžiūnas")

val FullName(meFirst, meLast) = me
//meFirst: String = Linas
//meLast: String = Medžiūnas

Notice a nice symmetry here: when constructing, me is on the left hand side, and FullName(...) with two string parameters is on the right hand side of the assignment. When deconstructing, it is exactly the opposite.

When talking about Scala pattern matching, the first thing that comes to mind is the match statement (which is similar to switch / case present in many other programming languages, just way more powerful, as we shall discover). However, there are many more places in Scala, where pattern matching comes to play: you can use it when defining lambda functions, also on the left hand side of for-comprehension generators, and even in assignment statements like in the example above. For the sake of simplicity, we will mostly use pattern matching within assignment statement in the rest of this post.

Now that we have our case class defined, and some code that is using it, we will try to understand what is so special about Scala case class, and what makes our code that’s using it to work. Sometimes, a really great way to understand how something works is to break it and then try to make it work again! Let’s do exactly that, by excluding the case keyword from the definition of our FullName class:

/*case*/ class FullName(first: String, last: String)

If you try this, you will find out that our code (both construction of value me, and deconstruction of it) no longer compiles. In order to fix it, we will need to manually implement the features that were kindly provided to us by Scala compiler before the things started breaking down. We add a companion object for the FullName class:

object FullName {

def apply(first: String, last: String): FullName =
new FullName(first, last)

def unapply(full: FullName): Some[(String, String)] =
Some((full.first, full.last))
}

Companion object in Scala is a singleton that has the same name and resides in the same file as it’s companion class. And, companion object and it’s class have an access to private members of each other. Companion object is where you put the static members of your class (unlike Java, Scala does not have the static modifier). This provides a more clear separation of static / instance members.

Note: we have to change our FullName class definition slightly, in order to make FullName.unapply compile:

/*case*/ class FullName(val first: String, val last: String)

Without this change, first and last would only serve as constructor arguments, and we wouldn’t be able to access them from unapply. Adding val before first and last turns them into both constructor arguments and instance fields (public by default) at the same time. This functionality, together with the companion object, was automatically generated for us by Scala compiler before we dropped the case keyword.

Adding all that code manually now is enough to fix what we have broken. Let’s get into the details of the two methods we have just implemented:

def apply(first: String, last: String): FullName

apply is a special method name in Scala, which by convention can be omitted from your code. So, FullName(...) is equivalent to FullName.apply(...). We are using it to construct new instances of FullName without needing the new keyword.

def unapply(full: FullName): Some[(String, String)]

unapply does the opposite — it deconstructs an instance of FullName, and is the foundation of pattern matching. We will focus on this method for the most of this article. In this case, it deconstructs FullName into two string values, and wraps them in Some, which means that it matches any instance of FullName (we will explore the partial matching a bit later).

Once again, notice the symmetry of these two methods: apply takes two strings as arguments, and returns an instance of FullName. unapply works in the opposite direction.

Now that we have a very basic understanding of what is unapply and how it can be used for deconstruction / pattern matching, let’s find a good use case for it. Turns out, it is not that easy! In most cases, it is already taken care of by Scala — implementations of unapply are provided not only for all the case classes that we write, but also for pretty much everything in the standard library of Scala, including collections (where applicable). And in fact, implementing your own unapply is not that common, unless you are a developer of some interesting library. However, we can cheat a bit — let’s take something from the world of Java, where unapply surely does not exist. Let’s take a few classes from java.time and add the support for Scala pattern matching on them:

import java.time.{LocalDate, LocalDateTime, LocalTime}

It sounds very natural to be able to deconstruct a Date into a year, a month and a day, and a Time into an hour, minutes and seconds. Also, a DateTime — into a Date and a Time. With the knowledge we already have, it is pretty much straightforward. The only important detail is that we cannot use the names LocalDate, LocalDateTime and LocalTime for creating the proper companion objects — they would need to reside in the same files as their classes, and since the classes come from Java standard library, it isn’t possible. In order to avoid name collisions, we simply omit Local from the names of the objects that we implement:

object DateTime {  def unapply(dt: LocalDateTime): Some[(LocalDate, LocalTime)] =
Some((dt.toLocalDate, dt.toLocalTime))
}

object Date {
def unapply(d: LocalDate): Some[(Int, Int, Int)] =
Some((d.getYear, d.getMonthValue, d.getDayOfMonth))
}

object Time {
def unapply(t: LocalTime): Some[(Int, Int, Int)] =
Some((t.getHour, t.getMinute, t.getSecond))
}

Let’s try using them:

val Date(year, month, day) = LocalDate.nowval Time(hour, minute, second) = LocalTime.now

Both LocalDate and LocalTime get deconstructed into 3 Int values each, as intended. And if we only need some of the deconstructed values and not the others, we can use underscore in place of those that we don’t need:

val Date(_, month, day) = LocalDate.now

A more interesting case is nested deconstruction of LocalDateTime:

val DateTime(Date(y, m, d), Time(h, mm, s)) = LocalDateTime.now

This gives us 6 Int values (3 for date part, and 3 for time).

Another feature of pattern matching that can sometimes be really useful is assignment of the whole value, which can be done besides the deconstruction. For our DateTime example it could look like this:

val dt @ DateTime(date @ Date(y, m, d), time @ Time(h, mm, s)) =
LocalDateTime.now

Besides the 6 Int values, we also get one value of LocalDate, one — of LocalTime, and finally the whole value of LocalDateTime (in dt).

In all the examples above, we were deconstructing into a fixed number of values — (year, month, day), or (hour, minute, second), or (date, time). There may be cases where we are working with a sequence of values, and not with some fixed number of them. This is also possible. I’ll try to illustrate that by deconstructing LocalDateTime into a sequence of Ints:

object DateTimeSeq {  def unapplySeq(dt: LocalDateTime): Some[Seq[Int]] =
Some(Seq(
dt.getYear, dt.getMonthValue, dt.getDayOfMonth,
dt.getHour, dt.getMinute, dt.getSecond))
}

unapplySeq is a variation of unapply that deconstructs into a sequence of values instead of a fixed size Tuple. In this example, the sequence always has a length of 6, but we can omit the tail of it in case we don’t need it:

val DateTimeSeq(year, month, day, hour, _*) = LocalDateTime.now

(by using _* which is the syntax of Scala varargs).

Until now, our unapply / unapplySeq was always returning a Some. It is time to introduce partial matching. For that, our unapply will need to return Some in case the value matches some criteria, and None — in case it does not. We are already working with values of LocalTime, and matching them into either AM or PM time would be a natural example:

object AM {  def unapply(t: LocalTime): Option[(Int, Int, Int)] =
t match {
case Time(h, m, s) if h < 12 => Some((h, m, s))
case _ => None
}
}

object PM {
def unapply(t: LocalTime): Option[(Int, Int, Int)] =
t match {
case Time(12, m, s) => Some(12, m, s)
case Time(h, m, s) if h > 12 => Some(h - 12, m, s)
case _ => None
}
}

Here, case _ => is the default case, which gets evaluated in case nothing else matched. Notice how we reused our previously definedTime.unapply under the hood of AM / PM? Also, we have just introduced two more features that we use for partial matching: guards (case Time(h, m, s) if h < 12), and matching by a constant (case Time(12, m, s)). I guess by now you already see what a powerful feature Scala pattern matching is!

To wrap this up, let’s implement ourselves a clock that formats the current time nicely, by using what we’ve learned about pattern matching, and our AM / PM extractors (plus some old school Java string formatting that almost looks like a stream of emojis):

LocalTime.now match {  case t @ AM(h, m, _) => 
f"$h%2d:$m%02d AM ($t precisely)"
case t @ PM(h, m, _) =>
f"$h%2d:$m%02d PM ($t precisely)"
}

We have explored most of the features of Scala pattern matching. You can find the code which this blog post was based on in this gist. For even better understanding, play around with it in Scala worksheet of IntelliJ IDEA or Scala IDE for Eclipse (or online, using Scastie). And if you have some complicated, nested ifs and elses in your Scala code, try to restructure it better, by using what you have learned about the pattern matching today.

--

--

Linas Medžiūnas
Wix Engineering

Software engineer at Chronosphere.io; unretired competitive programmer; 2x IOI gold medal winner; curious about Quantum algorithms.