Tour of Scala

Pattern Matching

Language

Pattern matching is a mechanism for checking a value against a pattern. A successful match can also deconstruct a value into its constituent parts. It is a more powerful version of the switch statement in Java and it can likewise be used in place of a series of if/else statements.

Syntax

A match expression has a value, the match keyword, and at least one case clause.

import scala.util.Random

val x: Int = Random.nextInt(10)

x match {
  case 0 => "zero"
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}
import scala.util.Random

val x: Int = Random.nextInt(10)

x match
  case 0 => "zero"
  case 1 => "one"
  case 2 => "two"
  case _ => "other"

The val x above is a random integer between 0 and 9. x becomes the left operand of the match operator and on the right is an expression with four cases. The last case _ is a “catch all” case for any other possible Int values. Cases are also called alternatives.

Match expressions have a value.

def matchTest(x: Int): String = x match {
  case 1 => "one"
  case 2 => "two"
  case _ => "other"
}
matchTest(3)  // returns other
matchTest(1)  // returns one
def matchTest(x: Int): String = x match
  case 1 => "one"
  case 2 => "two"
  case _ => "other"

matchTest(3)  // returns other
matchTest(1)  // returns one

This match expression has a type String because all of the cases return String. Therefore, the function matchTest returns a String.

Matching on case classes

Case classes are especially useful for pattern matching.

sealed trait Notification

case class Email(sender: String, title: String, body: String) extends Notification

case class SMS(caller: String, message: String) extends Notification

case class VoiceRecording(contactName: String, link: String) extends Notification

Notification is a sealed trait which has three concrete Notification types implemented with case classes Email, SMS, and VoiceRecording. (A sealed trait can be extended only in the same file as its declaration.) Now we can do pattern matching on these case classes:

def showNotification(notification: Notification): String = {
  notification match {
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
    case VoiceRecording(name, link) =>
      s"You received a Voice Recording from $name! Click the link to hear it: $link"
  }
}
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

println(showNotification(someSms))  // prints You got an SMS from 12345! Message: Are you there?

println(showNotification(someVoiceRecording))  // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
def showNotification(notification: Notification): String =
  notification match
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
    case VoiceRecording(name, link) =>
      s"You received a Voice Recording from $name! Click the link to hear it: $link"

val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

println(showNotification(someSms))  // prints You got an SMS from 12345! Message: Are you there?

println(showNotification(someVoiceRecording))  // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123

The function showNotification takes as a parameter the abstract type Notification and matches on the type of Notification (i.e. it figures out whether it’s an Email, SMS, or VoiceRecording). In the case Email(sender, title, _) the fields sender and title are used in the return value but the body field is ignored with _.

Matching on string

The s-interpolator allows embedding variables in strings and is also useful for pattern matching.

val input: String = "Alice is 25 years old"

input match {
  case s"$name is $age years old" => s"$name's age is $age"
  case _ => "No match"
}
// Result: "Alice's age is 25"
val input: String = "Alice is 25 years old"

input match
  case s"$name is $age years old" => s"$name's age is $age"
  case _ => "No match"
// Result: "Alice's age is 25"

In this example, name and age extract parts of the string based on the pattern. This is helpful for parsing structured text.

We can also use extractor objects for string pattern matching.

object Age {
  def unapply(s: String): Option[Int] = s.toIntOption
}

val input: String = "Alice is 25 years old"

val (name, age) = input match {
  case s"$name is ${Age(age)} years old" => (name, age)
}
// name: String = Alice
// age: Int = 25
object Age:
  def unapply(s: String): Option[Int] = s.toIntOption

val input: String = "Alice is 25 years old"

val (name, age) = input match
  case s"$name is ${Age(age)} years old" => (name, age)
// name: String = Alice
// age: Int = 25

Pattern guards

Pattern guards are boolean expressions which are used to make cases more specific. Just add if <boolean expression> after the pattern.

def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {
  notification match {
    case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>
      "You got an email from special someone!"
    case SMS(number, _) if importantPeopleInfo.contains(number) =>
      "You got an SMS from special someone!"
    case other =>
      showNotification(other) // nothing special, delegate to our original showNotification function
  }
}

val importantPeopleInfo = Seq("867-5309", "[email protected]")

val someSms = SMS("123-4567", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")
val importantSms = SMS("867-5309", "I'm here! Where are you?")

println(showImportantNotification(someSms, importantPeopleInfo)) // prints You got an SMS from 123-4567! Message: Are you there?
println(showImportantNotification(someVoiceRecording, importantPeopleInfo)) // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
println(showImportantNotification(importantEmail, importantPeopleInfo)) // prints You got an email from special someone!

println(showImportantNotification(importantSms, importantPeopleInfo)) // prints You got an SMS from special someone!
def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String =
  notification match
    case Email(sender, _, _) if importantPeopleInfo.contains(sender) =>
      "You got an email from special someone!"
    case SMS(number, _) if importantPeopleInfo.contains(number) =>
      "You got an SMS from special someone!"
    case other =>
      showNotification(other) // nothing special, delegate to our original showNotification function

val importantPeopleInfo = Seq("867-5309", "[email protected]")

val someSms = SMS("123-4567", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")
val importantSms = SMS("867-5309", "I'm here! Where are you?")

println(showImportantNotification(someSms, importantPeopleInfo)) // prints You got an SMS from 123-4567! Message: Are you there?
println(showImportantNotification(someVoiceRecording, importantPeopleInfo)) // prints You received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123
println(showImportantNotification(importantEmail, importantPeopleInfo)) // prints You got an email from special someone!

println(showImportantNotification(importantSms, importantPeopleInfo)) // prints You got an SMS from special someone!

In the case Email(sender, _, _) if importantPeopleInfo.contains(sender), the pattern is matched only if the sender is in the list of important people.

Matching on type only

You can match on the type like so:

sealed trait Device
case class Phone(model: String) extends Device {
  def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
  def screenSaverOn = "Turning screen saver on..."
}

def goIdle(device: Device): String = device match {
  case p: Phone => p.screenOff
  case c: Computer => c.screenSaverOn
}
sealed trait Device
case class Phone(model: String) extends Device:
  def screenOff = "Turning screen off"

case class Computer(model: String) extends Device:
  def screenSaverOn = "Turning screen saver on..."


def goIdle(device: Device): String = device match
  case p: Phone => p.screenOff
  case c: Computer => c.screenSaverOn

def goIdle has a different behavior depending on the type of Device. This is useful when the case needs to call a method on the pattern. It is a convention to use the first letter of the type as the case identifier (p and c in this case).

Binding matched patterns to variables

You can use variable binding to get type-dependent behavior while simultaneously extracting fields from the matched pattern.

def goIdleWithModel(device: Device): String = device match {
  case p @ Phone(model) => s"$model: ${p.screenOff}" 
  case c @ Computer(model) => s"$model: ${c.screenSaverOn}"
}
def goIdleWithModel(device: Device): String = device match
  case p @ Phone(model) => s"$model: ${p.screenOff}"
  case c @ Computer(model) => s"$model: ${c.screenSaverOn}"

Sealed types

You may have noticed that in the examples above the base types are qualified with the keyword sealed. This provides extra safety because the compiler checks that the cases of a match expression are exhaustive when the base type is sealed.

For instance, in the method showNotification defined above, if we forget one case, say, VoiceRecording, the compiler emits a warning:

def showNotification(notification: Notification): String = {
  notification match {
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
  }
}
def showNotification(notification: Notification): String =
  notification match
    case Email(sender, title, _) =>
      s"You got an email from $sender with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"

This definition produces the following warning:

match may not be exhaustive.

It would fail on pattern case: VoiceRecording(_, _)

The compiler even provides examples of input that would fail!

On the flip side, exhaustivity checking requires you to define all the subtypes of the base type in the same file as the base type (otherwise, the compiler would not know what are all the possible cases). For instance, if you try to define a new type of Notification outside of the file that defines the sealed trait Notification, it will produce a compilation error:

case class Telepathy(message: String) extends Notification
           ^
        Cannot extend sealed trait Notification in a different source file

Notes

Scala’s pattern matching statement is most useful for matching on algebraic types expressed via case classes. Scala also allows the definition of patterns independently of case classes, using unapply methods in extractor objects.

More resources

  • More details on match expressions in the Scala Book

Contributors to this page: