Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Pattern matching is a powerful feature of the Scala language. It allows for more concise and readable code while at the same time providing the ability to match elements against complex patterns.
In this tutorial, we’ll discover how to use pattern matching in general and how we can benefit from it.
In contrast with “exact matching” as we can have in Java’s switch statements, pattern matching allows matching a pattern instead of an exact value.
In Java for example, if input.equals(caseClause) returns false, we directly evaluate the next case clause. However, this is not how it always works in Scala.
Let’s discover more in detail how pattern matching works.
The match expressions consist of multiple parts:
Let’s take a look at a simple example to illustrate those parts:
def patternMatching(candidate: String): Int = {
candidate match {
case "One" => 1
case "Two" => 2
case _ => -1
}
}
Case classes help us use the power of inheritance to perform pattern matching. The case classes extend a common abstract class. The match expression then evaluates a reference of the abstract class against each pattern expressed by each case class.
Let’s begin by writing our classes:
abstract class Animal
case class Mammal(name: String, fromSea: Boolean) extends Animal
case class Bird(name: String) extends Animal
case class Fish(name: String) extends Animal
Let’s now see how we can apply pattern matching to case classes:
def caseClassesPatternMatching(animal: Animal): String = {
animal match {
case Mammal(name, fromSea) => s"I'm a $name, a kind of mammal. Am I from the sea? $fromSea"
case Bird(name) => s"I'm a $name, a kind of bird"
case _ => "I'm an unknown animal"
}
}
Another name for this kind of pattern matching is Constructor matching, meaning that the constructor is used in this case to make the match possible.
For instance, we can notice how the Mammal pattern matches exactly the constructor from the case class defined above.
Like most languages, Scala uses constants to define numbers or boolean values. Patterns can consist of constants.
Let’s see how we can use constants in a match expression:
def constantsPatternMatching(constant: Any): String = {
constant match {
case 0 => "I'm equal to zero"
case 4.5d => "I'm a double"
case false => "I'm the contrary of true"
case _ => s"I'm unknown and equal to $constant"
}
}
Arrays, Lists, and Vectors consist of elements. These sequences and their elements are also used to form patterns.
Moreover, we usually want to use wildcards to express the dynamic parts of the pattern:
Let’s see what a sequence looks like when used as a pattern:
def sequencesPatternMatching(sequence: Any): String = {
sequence match {
case List(singleElement) => s"I'm a list with one element: $singleElement"
case List(_, _*) => s"I'm a list with one or multiple elements: sequence"
case Vector(1, 2, _*) => s"I'm a vector: $sequence"
case _ => s"I'm an unrecognized sequence. My value: $sequence"
}
}
In the first case clause, we’ve used an alias – singleElement – to define the single element of the List:
In the other case clauses, we’re simply ignoring the values by using the underscore character. Aside from being used in default case clauses, the underscore character can also be used when a particular value is ignored in the match expression.
Tuples are objects containing a limited number of sub-objects. We can imagine those as collections of mixed elements with a limited size.
Next, let’s look at an example of how to use tuples in pattern matching:
def tuplesPatternMatching(tuple: Any): String = {
tuple match {
case (first, second) => s"I'm a tuple with two elements: $first & $second"
case (first, second, third) => s"I'm a tuple with three elements: $first & $second & $third"
case _ => s"Unrecognized pattern. My value: $tuple"
}
}
In this example, we’ve extracted the elements using the names defined in the tuple patterns. If we want to use the first element of the tuple, we’ll define a local variable in the tuple pattern. In the example above we can recognize those variables as first, second, and third.
Scala is a typed language, meaning that each object has a static type that cannot be changed. For instance, a Boolean object can only contain a boolean expression.
Scala makes it easy to match objects against type patterns, as shown below:
def typedPatternMatching(any: Any): String = {
any match {
case string: String => s"I'm a string. My value: $string"
case integer: Int => s"I'm an integer. My value: $integer"
case _ => s"I'm from an unknown type. My value: $any"
}
}
We already know how useful regular expressions are when working with strings of characters. In Scala, there’s good news — we can also use regular expressions when matching objects in our match expressions:
def regexPatterns(toMatch: String): String = {
val numeric = """([0-9]+)""".r
val alphabetic = """([a-zA-Z]+)""".r
val alphanumeric = """([a-zA-Z0-9]+)""".r
toMatch match {
case numeric(value) => s"I'm a numeric with value $value"
case alphabetic(value) => s"I'm an alphabetic with value $value"
case alphanumeric(value) => s"I'm an alphanumeric with value $value"
case _ => s"I contain other characters than alphanumerics. My value $toMatch"
}
}
In the previous section, we explored pattern matching using regular expressions. However, in many situations, the complexity of regular expressions isn’t necessary. Often, it’s sufficient to match simpler patterns within strings. We can achieve this by combining string interpolation with pattern matching to extract the relevant parts:
def stringInterpolationMatching(toMatch: String): String = {
toMatch match {
case s"$firstName.$lastName@$domain.$extension" =>
s"Hey ${firstName.capitalize} ${lastName.capitalize}, $domain.$extension is your email domain"
case s"$day-$month-${year}T$time" => s"$month $day, $year"
case s"$something($parenthesis)${_}" => s"String between parenthesis: $parenthesis"
case _ => "unknown pattern"
}
}
In the above example, we utilized simple string interpolation within the case statement. Extracting text between parentheses is a common task in data analysis, typically handled using regular expressions. However, we demonstrated how to do this without regular expressions. This approach allows us to effortlessly extract different parts of the string into separate variables based on the case pattern.
In functional languages like Scala, options are structures that either contain a value or not. An Option in Scala can easily be compared to Java’s Optional class.
Pattern matching is possible using Option objects. In this case, we’ll have two possible case clauses:
Let’s see how we’ll use those in our match expressions:
def optionsPatternMatching(option: Option[String]): String = {
option match {
case Some(value) => s"I'm not an empty option. Value $value"
case None => "I'm an empty option"
}
}
It is also possible to bind a variable to either a full or partial match results using the @ symbol:
def binderPatternMatching(animal: Any): String = {
animal match {
case m@Mammal(_, true) => s"${m.name} is a mammal from sea"
case Mammal(name, fromSea) => s"${name} is a mammal, fromSea:${fromSea}"
case _ => "unknown animal"
}
}
In this case, the variable m now is assigned to any Mammal object having the fromSea as true.
Similarly, we can bind a partial part of a matched object to a variable. Let’s say we want only the value Lion is assigned to the variable name:
def binderPatternWithPartMatch(animal: Any): String = {
animal match {
case Mammal(name @ "Lion", _) => s"$name is a mammal"
case _ => "unknown"
}
}
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.
Let’s see how convenient it can be to use a pattern guard:
def patternGuards(toMatch: Any, maxLength: Int): String = {
toMatch match {
case list: List[Any] if (list.size <= maxLength) => "List is of acceptable size"
case list: List[Any] => "List has not an acceptable size"
case string: String if (string.length <= maxLength) => "String is of acceptable size"
case string: String => "String has not an acceptable size"
case _ => "Input is neither a List nor a String"
}
}
In the example snippet of code, we notice two patterns each for both String and List objects. The difference between each lies in the fact that one of the patterns also checks on the length of the object before entering the case clause.
For instance, if a List contains 5 objects but the maximal length is 6, then it will enter the first case clause and return. On the other hand, if the maximal length were to be 4, then it would execute the code from the second clause.
We sometimes want to use sealed classes together with pattern matching. A sealed class is a superclass that is aware of every single class extending it. This behavior is possible using the same single file to express the sealed class and all of its subclasses.
This feature is particularly useful when we want to avoid having a default behavior in our match expression.
Let’s begin by writing our sealed class and its child classes:
sealed abstract class CardSuit
case class Spike() extends CardSuit
case class Diamond() extends CardSuit
case class Heart() extends CardSuit
case class Club() extends CardSuit
After that, let’s use pattern matching with these classes:
def sealedClass(cardSuit: CardSuit): String = {
cardSuit match {
case Spike() => "Card is spike"
case Club() => "Card is club"
case Heart() => "Card is heart"
case Diamond() => "Card is diamond"
}
}
Here, the usage of a default case clause is not mandatory as we have a case for each subclass.
Extractor objects are objects containing a method called unapply. This method is executed when matching against a pattern is successful.
Let’s use this in an example. Suppose we have a Person containing a full name, and when the Person is matched against a pattern, instead of using the full name, we just want their initials.
Let’s see how extractors can be useful when implementing this requirement.
Firstly, we’ll create the Person object:
object Person {
def apply(fullName: String) = fullName
def unapply(fullName: String): Option[String] = {
if (!fullName.isEmpty)
Some(fullName.replaceAll("(?<=\\w)(\\w+)", "."))
else
None
}
}
Now that the Person object exists, we’ll be able to use in our match expression and make use of the result of the unapply method:
def extractors(person: Any): String = {
person match {
case Person(initials) => s"My initials are $initials"
case _ => "Could not extract initials"
}
}
If the person is named John Smith, in this case, the returned String would be ‘My initials are J. S.‘.
A closure can also use pattern matching.
Let’s see how closure pattern matching looks in practice:
def closuresPatternMatching(list: List[Any]): List[Any] = {
list.collect { case i: Int if (i < 10) => i }
}
When invoked, this piece of code will:
We’re also able to use pattern matching to handle the exceptions thrown in try-catch blocks.
Let’s see this in action:
def catchBlocksPatternMatching(exception: Exception): String = {
try {
throw exception
} catch {
case ex: IllegalArgumentException => "It's an IllegalArgumentException"
case ex: RuntimeException => "It's a RuntimeException"
case _ => "It's an unknown kind of exception"
}
}
In this tutorial, we’ve discovered how to use Scala’s powerful pattern matching in many different ways.