8 Scala Pattern Matching Tricks
You can’t be a Scala programmer and say you’ve never used pattern matching.
Pattern matching is one of the most powerful Scala features. It allows one to test lots of values and conditions without nested and chained if-else expressions.
val aNumber = 44
val ordinal = aNumber match {
case 1 => "first"
case 2 => "second"
case 3 => "third"
case _ => aNumber + "th" // ignore the English grammar will you please
}
However, you also probably know that pattern matching is more than a switch expression, but more like a switch on steroids:
case class Person(name: String, age: Int, favoriteMovies: List[String])
val bob = Person("Bob", 34, List("Inception", "The Departed"))
val describeBob = bob match {
case Person(n, a, movies) => s"$n is $a years old and likes ${movies.mkString(",")}"
case _ => "I don't know what you're talking about"
}
In other words, pattern matching can deconstruct case class instances into their consistuent parts, which we can then reuse in the resulting expression on the right-hand side of =>.
Let me give you some more powerful patterns that Scala supports out of the box, but you might not have known existed.
1. List extractors
Lists can be deconstructed with pattern matching in a number of powerful ways. Let me build a list:
val countingList = List(1,2,3,42)
You can extract any element out of this list with a pattern that looks like the case class constructor:
val mustHaveThree = countingList match {
case List(_, _, 3, somethingElse) => s"A-HA! I've got a list with 3 as third element, I found $somethingElse after"
}
This pattern matches a list with exactly 4 elements, in which we don’t care about the first two, the third one must be exactly 3, and the fourth can be anything, but we name it somethingElse so we can reuse it in the s-interpolated string.
2. Haskell-like prepending
If I consider the same list as before, I can extract the head and tail of the list as follows:
val startsWithOne = countingList match {
case 1 :: someOtherElements => "This lists starts with one, the rest is $someOtherElements"
}
Don’t ask how this is possible yet - that will be the subject of an upcoming advanced article. The prepend pattern is often very useful in code that processes a list, but when you don’t know in advance whether the list is empty or not, so you write it like this:
def processList(numbers: List[Int]): String = numbers match {
case Nil => ""
case h :: t => h + " " + processList(t)
}
This style of handling a list may be very familiar to those of you who know a bit of Haskell.
3. List vararg pattern
The first pattern we showed above can only constrain a list to a definitive number of elements. What if you don’t know (or care about) the number of elements in advance?
val dontCareAboutTheRest = countingList match {
case List(_, 2, _*) => "I only care that this list has 2 as second element"
}
The * is the important bit, which means “any number of additional arguments”. This pattern is much more flexible because an (almost) infinite number of lists can match this pattern, instead of the 4-element list pattern we had before. The only catch with _* is that it must be the last bit in the pattern. In other words, the case List(, 2, _*, 55) will not compile, for example.
4. Other list infix patterns
It’s very useful when we can test the head of the list, or even the elements inside the list. But what if we want to test the last element of the list?
val mustEndWithMeaningOfLife = countingList match {
case List(1,2,_) :+ 42 => "found the meaning of life!"
}
The :+ is the append operator, which works much like :: from the point of view of pattern matching. You can also use the +: prepend operator (for symmetry), but we prefer ::. A nice benefit of the append operator is we can combine it with the vararg pattern for a really powerful structure:
val mustEndWithMeaningOfLife2 = countingList match {
case List(1, _*) :+ 42 => "I really don't care what comes before the meaning of life"
}
Which overcomes some of the limitations of the vararg pattern above.
5. Type specifiers
Sometimes we really don’t care about the values being matched, but only their type.
def gimmeAValue(): Any = { ... }
val gimmeTheType = gimmeAValue() match {
case _: String => "I have a string"
case _: Int => "I have a number"
case _ => "I have something else"
}
The :String bit is the important part. It allows the cases to match only those patterns which conform to that type. One very useful scenario where this is particularly useful is when we catch exceptions:
try {
...
} catch {
case _: IOException => "IO failed!"
case _: Exception => "We could have prevented that!"
case _: RuntimeException => "Something else crashed!"
}
(spoiler: catching exceptions is also based on pattern matching!)
The drawback with type guards is that they are based on reflection. Beware of performance hits!
6. Name binding
I’ve seen the following pattern more times than I can count:
def requestMoreInfo(p: Person): String = { ... }
val bob = Person("Bob", 34, List("Inception", "The Departed"))
val bobsInfo = bob match {
case Person(name, age, movies) => s"$name's info: ${requestMoreInfo(Person(name, age, movies))}"
}
We deconstruct a case class only to re-instantiate it with the same data for later. If we didn’t care about any field in the case class, that would be fine, because we would use a type specifier (see above). Even that is not 100% fine because we rely on reflection. But what if we care about some fields (not all) and the entire instance, so we can reuse those?
val bobsInfo = bob match {
case p @ Person(name, _, _) => s"$name's info: ${requestMoreInfo(p)}"
}
Answer: name the pattern you’re matching (see the p @) so you can reuse it later. You can even name sub-patterns:
val bobsInception = bob match {
case Person(name, _, movies @ List("Inception", _*)) => s"$name REALLY likes Inception, some other movies too: $movies"
}
7. Conditional guards
If you’re like me, you probably tried at least once to pattern match something that satisfies a condition, and because you only knew the “anything” and “constant” patterns, you gave up pattern matching and used chained if-elses instead.
val ordinal = gimmeANumber() match {
case 1 => "first"
case 2 => "second"
case 3 => "third"
case n if n % 10 == 1 => n + "st"
case n if n % 10 == 2 => n + "nd"
case n if n % 10 == 3 => n + "rd"
case n => n + "th"
}
As you can see above, the if guards are there directly in the pattern. Also notice that the condition does not have parentheses.
8. Alternative patterns
For the situations where you return the same expression for multiple patterns, you don’t need to copy and paste the same code.
val myOptimalList = numbers match {
case List(1, _, _) => "I like this list"
case List(43, _*) => "I like this list"
case _ => "I don't like it"
}
You can combine the patterns where you return the same expression into a single pattern:
val myOptimalList = numbers match {
case List(1, _, _) | List (43, _*) => "I like this list"
case _ => "I don't like it"
}
The only drawback of this pattern is that you can’t bind any names, because the compiler can’t ensure those values are available on the right-hand side.
This pattern is useful in practice for a lot of scenarios, for example when you want to handle many kinds of exceptions:
try {
...
} catch {
case _: RuntimeException | _: IOException => ""
}