1.  Overview

When we write programs, we need to control their flow conditionally. Not every case can be naturally represented as boolean decisions. Most programming languages have a mathematics-inspired feature that facilitates writing code that chooses between multiple cases.

Initially introduced in C and later adopted by Java, the switch statement is one of the first implementations of case analysis in a programming language. However, it’s essential to be aware of its limitations. First, it’s a statement, and second, it requires a break statement. Forgetting the break statement can cause the execution to fall through, potentially resulting in buggy behavior.

The “fall-through” feature has one legitimate use. Developers can write several case labels in succession without a break statement between them, leading to a shared response for varied inputs.

Functional programming languages like Scala solve this issue by adopting expression-oriented pattern matching and removing the fall-through behavior.

In this tutorial, we’ll learn how Scala allows us to write code that shares logic among multiple patterns.

2. Handling Multiple Patterns with Scala Pattern Matching

Scala’s pattern-matching mechanism is powerful, flexible, and remarkably elegant. It allows us to write clear flow control code.

A complete discussion of Scala’s pattern-matching or its destructuring capabilities is beyond the scope of this article. In this section, we’ll focus on how to match multiple patterns for each type supported in the language.

Scala allows us to specify multiple potential matches for a single case using the pipe | symbol. This approach allows several distinct values to trigger the same block of code, thus eliminating the need for redundant code and reducing the likelihood of errors that often arise from more verbose conditional structures:

sealed trait Command
case object Start extends Command
case object Stop extends Command
case object Report extends Command
case class CustomCommand(cmd: String) extends Command

def executeCommand(command: Command): String = command match {
  case Start | CustomCommand("begin") =>
    "System Starting."
  case Stop | CustomCommand("halt") =>
    "System Stopping."
  case Report | CustomCommand("status") =>
    "Generating Report."
  case _ =>
    "Unknown Command."
}

It’s an elegant solution that overloads the logical or operator; we can read the first case as if the command is Start or CustomCommand with the text “begins”.

We’re not limited to constructor patterns when grouping them this way. We can also use it with literal patterns:

def httpResponse(response: Int): String = response match {
  case 200 | 201 | 202 => "Success"
  case 400 | 404 | 500 => "Error"
  case _ => "Unknown status"
}

We can use any other pattern except wildcard or variable patterns, we need to be careful with type patterns because we can’t use variable names. The compiler will complain if we try it:

def multipleTypePatterns(obj: Any): String = obj match {
  case i: Int | s: String => println("This will not compile.")
  case _ => println("Other")
}

The compiler will fail in the above code with a Type Mismatch Error. We can still test for multiple types but can’t declare variable names for it:

def multipleTypePatterns(obj: Any): String = obj match {
  case _: String | _: Int => "It's either a String or an Int"
  case _ => "It's something else"
}

Scala 3 introduced union types, which gave us a new option to write code like the one above. Despite achieving the same result, it’s  not a multi-pattern but a single one matching against a type that can be either of the options:

def unionTypePattern(obj: Any): String = obj match {
  case _: (String | Int) => "It's either a String or an Int"
  case _ => "It's something else"
}

This approach is better for matching multiple types because it’s more succinct and clarifies the code’s intention.

3. Conclusion

In this article, we’ve reviewed Scala’s powerful and expressive pattern-matching feature and how it elevates the language’s ability to handle complex logic with simplicity and elegance. Unlike the traditional switch statements in many other programming languages, Scala’s pattern matching avoids common pitfalls such as fall-through errors. It offers a more robust, type-safe approach to handling various conditions and types.

Moreover, Scala’s flexibility in handling multiple patterns, whether through literals, types, or even complex structures like case classes, showcases its strength in functional programming.

The introduction of union types in Scala 3 further expands these capabilities, allowing programmers to succinctly express conditions involving multiple potential types within a single pattern.

As always, the code companion to the article can be found over on GitHub.