7 minutes read

Introduction

Imagine that we want to write a program that outputs the day of the week according to a number that we input. We could implement this with the help of the branching construction like the one in the example below:

if i == 0 then "Monday"
else if i == 1 then "Tuesday"
else if i == 2 then "Wednesday"
else if i == 3 then "Thursday"
else if i == 4 then "Friday"
else if i == 5 then "Saturday"
else "Sunday"

This code looks a bit too repetitive. Moreover, with cases when we have more than seven variants (e.g. months) there will be some really error-prone code structures. Is it possible to match one value to another with fewer overheads? Yes, in Scala we can do just that with matching.

Match expressions

In Scala you can match input value by patterns with the help of the match keyword. The simplest pattern is the explicit value. Let's describe the case with the weekdays using matching:

i match {
  case 0 => "Monday"
  case 1 => "Tuesday"
  case 2 => "Wednesday"
  case 3 => "Thursday"
  case 4 => "Friday"
  case 5 => "Saturday"
  case _ => "Sunday"
}

As you can see, we still have to input the value of i, but the difference from the if-else-if structure is in the series of patterns we use to assign the resulting value to the input. Each pattern starts with case and has to contain a pattern and a result after =>. The input will be processed over patterns from top to bottom and the detected match will be returned as the result. We could have a special pattern with the _ symbol representing a match for every input. It can play the role of the default result (if none of the patterns above matches the input, the default result will serve as the output).

Note that in Scala 3 curly brackets can be omitted if it makes the code more readable.

The example above is for matching to a specific value. But we could match the input with the different types of values:

scala>   1 match
     |     case 1.0 => println("double one")
     |     case _ => println("not double one")
double one

In the example above we are matching Int value with Double. We have a match since they are equal from a mathematical perspective. Note, however, that we could have action as the result of a match and return Unit. We could match input only by type like in the example below:

scala>   "test" match
     |     case s: String =>
     |       val l = s.length  
     |       println(s"string with length $l")
string with length 4

In this example, the result of matching for String type outputs a string. As you can see, we could have code blocks after =>, not just one string.

Alternatives

We can assign one result to a set of patterns:

i match
  case 1 | 3 | 5 => "odd"
  case 2 | 4 | 6 => "even"

In the example above we use | (or) operator to have alternatives for odd and even numbers. We can combine any type of pattern to simplify our code. We can use additional filtering with if as well:

i match
  case x if x % 2 != 0 => "odd"
  case _ => "even"

Here we check if the modulo of input is not zero as part of the case value if expression that returns a boolean result =>.

Collections matching

We could match collection types with patterns. For List we can use the :: operator:

scala>   List(1, 2, 3) match
     |     case 1 :: _ => println(s"head is 1")
     |     case _ =>
head is 1

In the example above we match the head element of List with 1. We can check additional properties of the collections with the help of if:

scala>   Set(2, 3, 4) match
     |     case s if s.contains(5) => println("contains 5")
     |     case s if s.size > 2 => println("size > 2")
size > 2

Here we match the set if it contains 5 to one result and if size more 2 to another. As we have no 5 at input we try second one pattern and success.

Correctness and exhaustivity

Not all the variants of matching are allowed by the compiler. If you try to match String to Int, you will get an error:

"test" match { case _: Int => println("int") }
// Error: this case is unreachable since type String is not a subclass of class Integer

Note that the compiler can't check all the variants, like Char can be matched with associated Int value:

scala>   'a' match
     |     case 97 => println("matched")
matched

You may have spotted that not all of the examples above show total matching, meaning that not all the cases might be covered:

def evenOrOdd(i: Int): String = i match
  case 1 | 3 | 5 => "odd"
  case 2 | 4 | 6 => "even"

What will happen if we call evenOrOdd with 7 or 0? We will receive a match error:

scala> evenOrOdd(7)
// scala.MatchError: 7 (of class java.lang.Integer)

A situation when matching (and assigned function if have one) has gaps in defining a property we can call exhaustivity. We can call exhaustive matching the one that can't produce MatchError.

Conclusion

Pattern matching is a helpful technique that simplifies a lot of the Scala code. We can assign a resulting value or action for the input with the match pattern, construct a sequence of patterns and get a result from top to bottom. A pattern can be an explicit value, a type of value, a set of alternatives with |. We can add additional filtering to the pattern with if. We could use pattern matching directly with a code block or with an assigned function. To be on the safe side it is better to define exhaustive matching; this will prevent you from getting MatchError.

9 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo