What is Pattern Matching?
Pattern matching is a powerful feature of the Scala language. It is a mechanism for checking a value against a pattern. A successful match deconstructs the value into its constituent parts.
Pattern matching allows for more concise and readable code while at the same time provide the ability to match elements against complex patterns.
In this blog, we will see the power of the Scala’s pattern matching in different use cases.
The match expression consist of multiple parts:
- The value we’ll use to match the patterns is called a candidate
- The keyword match
- At least one case clause consisting of the case keyword, the pattern, an arrow symbol, and the code to be executed when the pattern matches
- A default clause when no other pattern has matched. The default clause is recognizable because it consists of the underscore character (_) and is the last of the case clauses
Here is a simple example to illustrate those parts:
Is match Better Replacement for the Java switch Statements and if/else Statements?
match expressions can be seen as a generalization of switch statements and if/else statements. match expressions do everything that switch statements and if/else statements do, and much more.
However, there are few major differences to keep in mind:
- First, match is an expression in Scala, it always results in a value.
- Second, match expressions can be used with any type. If you want to match on your own types, go right ahead!
- Third, Scala’s alternative expressions never “fall through” into the next case. In Java, we must use explicit break statements to exit a switch at the end of each branch, or we will fall through to the next branch. This is annoying and error-prone.
- Fourth, if none of the patterns match, an exception named MatchError is thrown. This means we always have to make sure that all cases are covered, even if it means adding a default case where there’s nothing to do.
Patterns in match Expressions
The wildcard pattern ( _ ) matches any object whatsoever. It is used as a default case i.e., catch-all alternative. It can also be used to represent the element of an object whose value is not required.
Here is an example to illustrate wildcard patterns.
A constant pattern matches only itself. Any literal may be used as a constant.
Here is an example to illustrate constant patterns.
A variable pattern matches any object, just like a wildcard. But unlike a wildcard, Scala binds the variable to whatever the object is. So then, we can use this variable to act on the object further.
Also note that, in case clauses, a term that begins with a lowercase letter is assumed to be the name of a new variable that will hold an extracted value. To refer to a previously defined variable, enclose it in back-ticks. Conversely, a term that begins with an uppercase letter is assumed to be a type name.
To avoid duplication, case clauses also support an “or” construct, using a | method.
Here is an example to illustrate variable patterns.
Constructors are where pattern matching becomes really powerful. Case classes are a special kind of classes that are optimized for use in pattern matching.
Pattern matching of case classes is also called deep matching, where we examine the contents of instances of case classes. Matching of case classes means to first check that the object is a member of the named case class, and then to check that the constructor parameters of the object match the extra patterns supplied.
Here is an example to illustrate constructor patterns.
We can also match against sequences. Arrays, Lists and Vectors consist of elements. These sequences and their elements are also used to form patterns.
We can use wildcards to specify any number of elements within the pattern. To match a single element, underscore wildcard ( _ ) is used. On the other hand, to match an unknown number of elements (zero, one, or more), star wildcard ( * ) is used.
Here is an example to illustrate sequence patterns.
List collection is little different than other collections. It is built from “cons” cells and ends in a Nil element. We can match against Lists with List expressions, using :: operator. List patterns are used to deconstruct the List into its constituent parts.
Here is an example to illustrate list patterns.
We can match against tuples too. Tuples are objects containing a limited number of sub-objects. We can imagine those as collections of mixed elements with a limited size. Tuple patterns are used to deconstruct the Tuple into its constituent parts.
Here is an example to illustrate tuple patterns.
Scala is a typed language, each object has a static type that cannot be changed. We can match on the type of an expression.
Here is an example to illustrate typed patterns.
The effect of match against a typed pattern is equivalent by using a type test followed by a type cast.
To test whether an expression has type String, we use: expr.isInstanceOf[String]
To cast the same expression to type String, we use:
Using a type test and type cast, we could rewrite the first case of the given match expression.
Option Type Patterns
Scala has a standard type named Option for optional values. Such a value can be of two forms: Some(x) object, where x is the actual value, or the None object, which represents a missing value.
Here is an example to illustrate option type patterns.
What are Pattern Guards?
We’ve already seen how powerful pattern matching can be. We can build our patterns in so many different ways.
But sometimes, we want to make sure a specific condition is fulfilled in addition to our pattern matching, to execute the code inside of the case clause. We can use pattern guards to achieve this behavior. Pattern guards are boolean expressions used together on the same level as the case clause.
Here is an example to illustrate pattern guards.
Matching with Sealed Classes
When we use pattern matching with case classes, we would like the compiler to check that we exhausted all alternatives. To achieve this, we can declare the common super class as sealed. A sealed class is a super class that is aware of every single class extending it. This behavior can be achieved by defining all sub classes of the sealed class in the same file as the class itself.
When a class is sealed, all of its sub classes are known at compile time, enabling the compiler to check pattern clauses for completeness.
This feature is particularly useful when we want to avoid having a default behavior in our match expression.
Here is an example to illustrate matching with sealed classes.
Matching with Extractors
Extractors are objects with an unapply or unapplySeq method that extract values from an object.
The unapply method is provided to extract a fixed number of objects, while unapplySeq extracts a sequence whose length can vary. These methods are executed when matching against a pattern is successful.
If we want to use pattern matching on one of our classes, but we do not want to open access to our classes the way case classes do, we can use the extractors.
Here is an example to illustrate matching with extractors.
Common Pattern Matching Errors and Warnings
Unreachable Code Warning
match expressions are eager, so more specific clauses must appear before less specific clauses. Otherwise, the more specific clauses will never get the chance to match. So, the default clause shown must be the last one. The compiler will catch the error, fortunately, and will give the warning of unreachable code.
If none of the patterns match with candidate, then compiler will throw an exception named MatchError.
Exhaustive Match Warning
If all alternatives are not exhausted in match expression using case clauses then compiler will throw a warning of match may not be exhaustive. This type of warning occurs if any sub class of sealed base class is not covered in case clauses of match expressions.
Pattern matching is a powerful “protocol” for extracting data inside data structures and objects. It makes idiomatic Scala code concise, yet powerful. Though, it’s not unusual for Scala programs to have 10 times fewer lines of code than comparable programs written in Java.
All the source code used in this tutorial can be found over on GitHub.