1. Overview

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.

2. Pattern Matching

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.

2.1. Syntax

The match expressions consist of multiple parts:

  1. The value we’ll use to match the patterns is called a candidate 
  2. The keyword match
  3. Multiple case clauses consisting of the case keyword, the pattern, an arrow symbol, and the code to execute when the pattern matches
  4. 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

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 
  }
}

3. Patterns in Match Expression

3.1. Case Classes

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.

3.2. Constants

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"
  }
}

3.3. Sequences

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:

  • To match a single element, we’ll use the underscore wildcard _. This is not to be confused with the default clause, which also uses the underscore character. An alias can also be used to represent the element
  • On the other hand, to match an unknown number of elements (zero, one, or more), we’ll use the star wildcard *

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.

3.4. Tuples

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.

3.5. Typed Patterns

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"
  }
}

3.6. Regex Patterns

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"
  }
}

3.7. String Interpolation Patterns

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.

3.8. Options: Some and None

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:

  • Some — containing a value of type T
  • None — not containing anything

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"
  }
}

3.9. Variable Binding

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"
  }
}

4. 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.

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.

5. Sealed Classes

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.

6. Extractors

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.*‘.

7. Other Usages

7.1. Closures

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:

  1. Filter out the elements that are not an Int
  2. Filter out Ints less than 10
  3. Collect the remaining elements from the List in a new List

7.2. Catch Blocks

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"
  }
}

8. Conclusion

In this tutorial, we’ve discovered how to use Scala’s powerful pattern matching in many different ways.

As usual, all the source code used in this tutorial can be found over on GitHub.