8 Scala Pattern Matching Tricks
Join the DZone community and get the full member experience.
Join For FreeYou can't be a Scala programmer and say you've never used pattern matching — it 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.
This article is for the Scala programmer who is either, a) only getting started with Scala, or b) has only used pattern matching in its basic form — as a switch-on-steroids or to deconstruct case classes.
This article will give you some tools to use pattern matching to the max. You can also watch it in video form on YouTube or attached below:
1. List Extractors
Lists can be deconstructed with pattern matching in a number of powerful ways. Let me build a list:
xxxxxxxxxx
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:
xxxxxxxxxx
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 four 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:
xxxxxxxxxx
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, you write it like this:
xxxxxxxxxx
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?
xxxxxxxxxx
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?
xxxxxxxxxx
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 that we can combine it with the vararg pattern for a really powerful structure:
xxxxxxxxxx
val mustEndWithMeaningOfLife2 = countingList match {
case List(1, _*) :+ 42 => "I really don't care what comes before the meaning of life"
}
(Look for the _*
), 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.
xxxxxxxxxx
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 that conform to that type. One very useful scenario where this is particularly useful is when we catch exceptions:
xxxxxxxxxx
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:
xxxxxxxxxx
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. Can we reuse those?
xxxxxxxxxx
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:
xxxxxxxxxx
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.
xxxxxxxxxx
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 situations where you return the same expression for multiple patterns, you don't need to copy and paste the same code.
xxxxxxxxxx
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:
xxxxxxxxxx
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:
xxxxxxxxxx
try {
...
} catch {
case _: RuntimeException | _: IOException => ""
}
Until Next Time
I hope it was useful, and you're better equipped to use pattern matching to the fullest! I've just started writing here and on the Rock the JVM blog, so leave your feedback in the comments, I read everything.
If you liked this, you can also read the 2-hour Scala at Light Speed comprehensive mini-series on Scala and functional programming.
Published at DZone with permission of Daniel Ciocirlan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments