Pattern matching is a powerful tool for control flow in Scala. It is often underused and under-estimated by people coming to Scala from imperative languages.
Let's start with a few examples of pattern matching before diving into the theory. We start by defining a tuple:
scala> val names = ("Pascal", "Bugnion") names: (String, String) = (Pascal,Bugnion)
We can use pattern matching to extract the elements of this tuple and bind them to variables:
scala> val (firstName, lastName) = names firstName: String = Pascal lastName: String = Bugnion
We just extracted the two elements of the names
tuple, binding them to the variables firstName
and lastName
. Notice how the left-hand side defines a pattern that the right-hand side must match: we are declaring that the variable names
must be a two-element tuple. To make the pattern more specific, we could also have specified the expected types of the elements in the tuple:
scala> val (firstName:String, lastName:String) = names firstName: String = Pascal lastName: String = Bugnion
What happens if the pattern on the left-hand side does not match the right-hand side?
scala> val (firstName, middleName, lastName) = names <console>:13: error: constructor cannot be instantiated to expected type; found : (T1, T2, T3) required: (String, String) val (firstName, middleName, lastName) = names
This results in a compile error. Other types of pattern matching failures result in runtime errors.
Pattern matching is very expressive. To achieve the same behavior without pattern matching, you would have to do the following explicitly:
names
is a two-element tuplefirstName
lastName
If we expect certain elements in the tuple to have specific values, we can verify this as part of the pattern match. For instance, we can verify that the first element of the names
tuple matches "Pascal"
:
scala> val ("Pascal", lastName) = names lastName: String = Bugnion
Besides tuples, we can also match on Scala collections:
scala> val point = Array(1, 2, 3) point: Array[Int] = Array(1, 2, 3) scala> val Array(x, y, z) = point x: Int = 1 y: Int = 2 z: Int = 3
Notice the similarity between this pattern matching and array construction:
scala> val point = Array(x, y, z) point: Array[Int] = Array(1, 2, 3)
Syntactically, Scala expresses pattern matching as the reverse process to instance construction. We can think of pattern matching as the deconstruction of an object, binding the object's constituent parts to variables.
When matching against collections, one is sometimes only interested in matching the first element, or the first few elements, and discarding the rest of the collection, whatever its length. The operator _*
will match against any number of elements:
scala> val Array(x, _*) = point x: Int = 1
By default, the part of the pattern matched by the _*
operator is not bound to a variable. We can capture it as follows:
scala> val Array(x, xs @ _*) = point x: Int = 1 xs: Seq[Int] = Vector(2, 3)
Besides tuples and collections, we can also match against case classes. Let's start by defining a case representing a name:
scala> case class Name(first: String, last: String) defined class Name scala> val name = Name("Martin", "Odersky") name: Name = Name(Martin,Odersky)
We can match against instances of Name
in much the same way we matched against tuples:
scala> val Name(firstName, lastName) = name firstName: String = Martin lastName: String = Odersky
All these patterns can also be used in match
statements:
scala> def greet(name:Name) = name match { case Name("Martin", "Odersky") => "An honor to meet you" case Name(first, "Bugnion") => "Wow! A family member!" case Name(first, last) => s"Hello, $first" } greet: (name: Name)String
Pattern matching is useful in for comprehensions for extracting items from a collection that match a specific pattern. Let's build a collection of Name
instances:
scala> val names = List(Name("Martin", "Odersky"), Name("Derek", "Wyatt")) names: List[Name] = List(Name(Martin,Odersky), Name(Derek,Wyatt))
We can use pattern matching to extract the internals of the class in a for-comprehension:
scala> for { Name(first, last) <- names } yield first List[String] = List(Martin, Derek)
So far, nothing terribly ground-breaking. But what if we wanted to extract the surname of everyone whose first name is "Martin"
?
scala> for { Name("Martin", last) <- names } yield last List[String] = List(Odersky)
Writing Name("Martin", last) <- names
extracts the elements of names that match the pattern. You might think that this is a contrived example, and it is, but the examples in Chapter 7, Web APIs demonstrate the usefulness and versatility of this language pattern, for instance, for extracting specific fields from JSON objects.