4.
Pattern Matching
Written by Ehab Amer
In this chapter, you’ll learn about proper golf attire: How to pair a striped shirt with plaid shorts:
No, just playing! This is not your grandfather’s pattern matching.
You’ve already seen pattern matching in action. In “Swift Apprentice: Fundamentals - Chapter 4: Advanced Control Flow”, you used a switch statement to match numbers and strings in different cases. That’s a simple example, but there’s a lot more to explore on the topic.
You’ll dive deep into the underlying mechanisms and understand more about how the Swift compiler interprets the code you type.
Swift is a multi-paradigm language that lets you build full-featured, production-ready, object-oriented software. The designers of Swift borrowed some tricks from more functional style languages like Haskell and Erlang.
Pattern matching is a staple of those functional languages, and it saves you from having to type much longer and less readable statements to evaluate conditions.
Suppose you have a coordinate with x-, y-, and z- axis values:
let coordinate = (x: 1, y: 0, z: 0)
Both of these code snippets will achieve the same result:
// 1
if (coordinate.y == 0) && (coordinate.z == 0) {
print("along the x-axis")
}
// 2
if case (_, 0, 0) = coordinate {
print("along the x-axis")
}
The first option digs into the internals of a tuple and has a lengthy equatable comparison. It also uses the logical && operator to ensure both conditions are true.
The second option, using pattern matching, is concise and readable.
The following sections will show you how — and when — to use patterns in your code.
Introducing Patterns
Patterns provide rules to match values. You can use patterns in switch cases, as well as in if, while, guard, and for statements. You can also use patterns in variable and constant declarations.
Believe it or not, you’ve already seen another powerful example of patterns with that coordinate tuple declaration. You construct a tuple by separating values with commas between parentheses, like (x, y, z). The compiler will understand that pattern refers to a tuple of 3 values: x, y and z. Tuples have the structure of a composite value.
Single values also have a structure. The number 42 is a single value and, by its very nature, identifiable.
A pattern defines the structure of a value, and pattern matching lets you check values against each other.
Note: The structure of a value doesn’t refer to the
structtype. They are different concepts, even though they use the same word. It could be a symptom of the paucity of language!
Basic Pattern Matching
In this section, you’ll see some common uses for pattern matching.
if and guard
You’ve used if and guard statements for flow control. You can transform them into pattern-matching statements using a case condition. The example below shows how you use an if statement with a case condition:
func process(point: (x: Int, y: Int, z: Int)) -> String {
if case (0, 0, 0) = point {
return "At origin"
}
return "Not at origin"
}
let point = (x: 0, y: 0, z: 0)
let status = process(point: point) // At origin
In that code, all three axes match zero values.
A case condition in a guard statement achieves the same effect:
func process(point: (x: Int, y: Int, z: Int)) -> String {
guard case (0, 0, 0) = point else {
return "Not at origin"
}
// guaranteed point is at the origin
return "At origin"
}
In a case condition, you write the pattern first, followed by an equals sign, =, and then the value you want to match to the pattern. if statements and guard statements work best if there is a single pattern you care to match.
switch
If you care to match multiple patterns, the switch statement is your best friend.
You can rewrite processPoint() like this:
func process(point: (x: Int, y: Int, z: Int)) -> String {
// 1
let closeRange = -2...2
let midRange = -5...5
// 2
switch point {
case (0, 0, 0):
return "At origin"
case (closeRange, closeRange, closeRange):
return "Very close to origin"
case (midRange, midRange, midRange):
return "Nearby origin"
default:
return "Not near origin"
}
}
let point = (x: 15, y: 5, z: 3)
let status = process(point: point) // Not near origin
This code introduces a couple of new concepts:
- You can match against ranges of numbers.
- The
switchstatement allows multiple cases to match patterns.
Because of its exhaustiveness checking, the switch statement also provides an advantage over the if statement. The compiler guarantees that you have checked for all possible values by the end of a switch statement.
Also, recall that a switch statement will exit with the first case condition that matches. That’s why you place the midRange condition second. Even though the midRange condition would match a closeRange value, it won’t evaluate unless the previous condition fails. The default case is the catch-all. The default case will execute if there hasn’t been a match in all the other cases.
Mini-exercise
Given the population of a group, write a switch statement that prints out a comment for different group sizes: single, a few, several and many.
for
A for loop churns through a collection of elements. Pattern matching can act as a filter:
let groupSizes = [1, 5, 4, 6, 2, 1, 3]
for case 1 in groupSizes {
print("Found an individual") // 2 times
}
In this example, the array provides a list of workgroup sizes for a school classroom. The loop’s body only runs for elements in the array that match the value 1. Since students in the class are encouraged to work in teams instead of individually, you can isolate those who have not found a partner.
Patterns
Now that you’ve seen some basic pattern-matching examples, let’s see more patterns you can match.
Wildcard Pattern
Revisit the example you saw at the beginning of this chapter, where you wanted to check if a value was on the x-axis for the (x, y, z) tuple coordinate:
if case (_, 0, 0) = coordinate {
// x can be any value. y and z must be exactly 0.
print("On the x-axis") // Printed!
}
The pattern in this case condition uses an underscore, _, to match any value of x component and exactly 0 for the y and z components.
Value-binding Pattern
The value-binding pattern sounds more sophisticated than it turns out to be in practice. You simply use var or let to declare a variable or a constant while matching a pattern.
You can then use the value of the variable or constant inside the execution block:
if case (let x, 0, 0) = coordinate {
print("On the x-axis at \(x)") // Printed: 1
}
The pattern in this case condition matches any value on the x-axis and binds its x component to the constant named x for use in the execution block.
If you wanted to bind multiple values, you could write let multiple times or, even better, move the let outside the tuple:
if case let (x, y, 0) = coordinate {
print("On the x-y plane at (\(x), \(y))") // Printed: 1, 0
}
The compiler will bind all the unknown constant names it finds by putting the let on the outside of the tuple.
Identifier Pattern
The identifier pattern is even more straightforward than the value-binding pattern. The identifier pattern is the constant or variable name itself; in the example above, that’s the x in the pattern. You’re telling the compiler, “When you find a value of (something, 0, 0), assign the something to x.”
This description feels intertwined with what you’ve seen before because the identifier pattern is a sub-pattern of the value-binding pattern.
Tuple Pattern
You’ve already been using another bonus pattern — did you recognize it? The tuple isn’t just a series of comma-separated values between parentheses: it’s comma-separated patterns. In the example tuple pattern, (something, 0, 0), the interior patterns are (identifier, expression, expression).
You’ll learn about expression patterns at the end of this chapter. For now, the important takeaway is that the tuple pattern combines many patterns into one and helps you write terse code.
Enumeration Case Pattern
In “Swift Apprentice: Fundamentals - Chapter 16: Enumerations”, you saw how you could match the member values of an enumeration:
enum Direction {
case north, south, east, west
}
let heading = Direction.north
if case .north = heading {
print("Don’t forget your jacket") // Printed!
}
As you can imagine, the enumeration case pattern matches the value of an enumeration. In this example, case .north will only match the .north value of the enumeration.
The enumeration case pattern has some magic up its sleeve. When you combine it with the value binding pattern, you can extract associated values from an enumeration:
enum Organism {
case plant
case animal(legs: Int)
}
let pet = Organism.animal(legs: 4)
switch pet {
case .animal(let legs):
print("Potentially cuddly with \(legs) legs") // Printed: 4
default:
print("No chance for cuddles")
}
In that code, the associated value for .animal is bound to the constant named legs. You reference the legs constant in the print call inside the execution block of that condition.
Associated values are locked away in enumeration values until you use the value-binding pattern to extract them.
Mini-exercise
In “Swift Apprentice: Fundamentals - Chapter 16: Enumerations”, you learned that an optional is an enumeration under the hood. An optional is either .some(value) or .none. You just learned how to extract associated values from optionals.
Given the following array of optionals, print the names that are not nil with a for loop:
let names: [String?] =
["Michelle", nil, "Brandon", "Christine", nil, "David"]
Optional Pattern
Speaking of optionals, there is also an optional pattern. The optional pattern consists of an identifier pattern followed immediately by a question mark. You can use this pattern in the same places you can use enumeration case patterns.
You can rewrite the solution to the mini-exercise as:
for case let name? in names {
print(name) // 4 times
}
Optional patterns are syntactic sugar for enumeration case patterns containing optional values. Syntactic sugar merely means a more pleasant way of writing the same thing.
“Is” Type-casting Pattern
Using the is operator in a case condition, you check if an instance is of a particular type. An example of when to use this is parsing through a JSON export. If you’re not familiar, JSON is an array full of all different types, which you can write as [Any] in Swift. Web APIs and website developers make use of JSON a lot.
Therefore, when you’re parsing data from a web API, you’ll need to check if each value is of a particular type:
let response: [Any] = [15, "George", 2.0]
for element in response {
switch element {
case is String:
print("Found a string") // 1 time
default:
print("Found something else") // 2 times
}
}
With this code, you find out that one of the elements is of type String, but you don’t have access to its value. That’s where the following pattern comes to the rescue.
“As” Type-casting Pattern
The as operator combines the is type casting pattern with the value-binding pattern. Extending the example above, you could write a case like this:
for element in response {
switch element {
case let text as String:
print("Found a string: \(text)") // 1 time
default:
print("Found something else") // 2 times
}
}
So when the compiler finds an object that it can cast to a String, it will bind the value to the text constant.
Advanced Patterns
You’ve blazed through all the above patterns! What you’ve learned so far in this chapter will carry you quite far as a developer. In the upcoming section, you’ll learn additional modifier tricks to consolidate your code.
Qualifying With where
You can specify a where condition to further filter a match by checking a unary condition in-line:
for number in 1...9 {
switch number {
case let x where x % 2 == 0:
print("even") // 4 times
default:
print("odd") // 5 times
}
}
If the number in the code above is divisible evenly by two, the first case matches.
You can utilize where in a more sophisticated way with enumerations. Imagine you’re writing a game where you want to save the player’s progress for each level:
enum LevelStatus {
case complete
case inProgress(percent: Double)
case notStarted
}
let levels: [LevelStatus] =
[.complete, .inProgress(percent: 0.9), .notStarted]
for level in levels {
switch level {
case .inProgress(let percent) where percent > 0.8 :
print("Almost there!")
case .inProgress(let percent) where percent > 0.5 :
print("Halfway there!")
case .inProgress(let percent) where percent > 0.2 :
print("Made it through the beginning!")
default:
break
}
}
In this code, one level in the game is currently in progress. That level matches the first case as 90% complete and prints "Almost there!". The where condition tests the associated value from the enumeration case.
Chaining With Commas
Another thing you learned was how to match multiple patterns in a single-case condition. Here’s an example similar to what you saw previously:
func timeOfDayDescription(hour: Int) -> String {
switch hour {
case 0, 1, 2, 3, 4, 5:
return "Early morning"
case 6, 7, 8, 9, 10, 11:
return "Morning"
case 12, 13, 14, 15, 16:
return "Afternoon"
case 17, 18, 19:
return "Evening"
case 20, 21, 22, 23:
return "Late evening"
default:
return "INVALID HOUR!"
}
}
let timeOfDay = timeOfDayDescription(hour: 12) // Afternoon
Here you see several identifier patterns matched in each case condition. You can list as many as you like, separated by commas.
The constants and variables you bind in a pattern are available in subsequent patterns. Here’s a refinement to the cuddly animal test:
if case .animal(let legs) = pet, case 2...4 = legs {
print("potentially cuddly") // Printed!
} else {
print("no chance for cuddles")
}
The first pattern, before the comma, binds the associated value of the enumeration to the constant legs. In the second pattern, after the comma, the value of the legs constant is matched against a range.
Swift’s if statement is surprisingly capable. An if statement can have multiple conditions, separated by commas. Conditions fall into one of three categories:
-
Simple logical test E.g.:
foo == 10 || bar > baz. -
Optional binding E.g.:
let foo = maybeFoo. -
Pattern matching E.g.:
case .bar(let value) = something.
Conditions evaluate in the order they are defined. At runtime, no conditions following a failing condition evaluate. Here is a contrived example of a complicated if statement:
enum Number {
case integerValue(Int)
case doubleValue(Double)
case booleanValue(Bool)
}
let a = 5
let b = 6
let c: Number? = .integerValue(7)
let d: Number? = .integerValue(8)
if a != b {
if let c = c {
if let d = d {
if case .integerValue(let cValue) = c {
if case .integerValue(let dValue) = d {
if dValue > cValue {
print("a and b are different") // Printed!
print("d is greater than c") // Printed!
print("sum: \(a + b + cValue + dValue)") // 26
}
}
}
}
}
}
Nesting all those if statements one inside the other is known as a pyramid of doom. Instead, you can use the unwrapped and bound values immediately after consecutive commas:
if a != b,
let c = c,
let d = d,
case .integerValue(let cValue) = c,
case .integerValue(let dValue) = d,
dValue > cValue {
print("a and b are different") // Printed!
print("d is greater than c") // Printed!
print("sum: \(a + b + cValue + dValue)") // Printed: 26
}
So now, you see that pattern matching can be combined with simple logical conditions and optional binding within a single if statement. Your code is looking more elegant already!
Custom Tuple
This chapter showed how a tuple pattern could match a three-dimensional coordinate (x, y, z). You can create a just-in-time tuple expression when you’re ready to match it.
Here’s a tuple that does just that:
let name = "Bob"
let age = 23
if case ("Bob", 23) = (name, age) {
print("Found the right Bob!") // Printed!
}
Here you combine the name and age constants into a tuple and evaluate them together.
Another such example involves a login form with a username and password field. Users are notorious for leaving fields incomplete and then clicking Submit. In these cases, you want to show a specific error message to the user that indicates the missing field, like so:
var username: String?
var password: String?
switch (username, password) {
case let (username?, password?):
print("Success! User: \(username) Pass: \(password)")
case let (username?, nil):
print("Password is missing. User: \(username)")
case let (nil, password?):
print("Username is missing. Pass: \(password)")
case (nil, nil):
print("Both username and password are missing") // Printed!
}
Each case checks one of the possible submissions. You write the success case first because there is no need to check the other cases to see if they are true. Swift’s switch statements don’t fall through, so the remaining conditions don’t evaluate if the first case condition is true.
Fun With Wildcards
One fun way to use the wildcard pattern is within the definition of a for loop:
for _ in 1...3 {
print("hi") // 3 times
}
This code performs its action three times. The underscore _ means that you don’t care to use each value from the sequence. If you ever need to repeat an action, this is a clean way to write the code.
Optional Existence Validate
let user: String? = "Bob"
guard let _ = user else {
print("There is no user.")
fatalError()
}
print("User exists, but identity not needed.") // Printed!
In this code, you check to make sure user has a value. You use the underscore to indicate that, right now, you don’t care what value it contains.
Even though you can do something, it doesn’t mean you should. The best way to validate an optional where you don’t care about the value is like so:
guard user != nil else {
print("There is no user.")
fatalError()
}
Here, user != nil does the same thing as let _ = user, but the intent is more apparent.
Organize an if-else-if
In app development, views are rectangles. Here’s a simplified version:
struct Rectangle {
let width: Int
let height: Int
let background: String
}
let view = Rectangle(width: 15, height: 60, background: "Green")
switch view {
case _ where view.height < 50:
print("Shorter than 50 units")
case _ where view.width > 20:
print("Over 50 tall, & over 20 wide")
case _ where view.background == "Green":
print("Over 50 tall, at most 20 wide, & green") // Printed!
default:
print("This view can’t be described by this example")
}
You could write this code as a chain of if statements. When you use the switch statement, it becomes clear that each condition is a case. Notice that each case uses an underscore with a qualifying where clause.
Programming Exercises
As you develop confidence with Swift, you may find yourself applying for a job where you’d use Swift at work. Hiring interviews have some classic questions like the Fibonacci and FizzBuzz algorithms. Pattern matching can come in handy for both of these challenges.
Note: Both algorithms are call-intensive. If you’re following along in a playground, please start a new playground and use it for the rest of this chapter to avoid it stuttering under the processing load.
Fibonacci
In the Fibonacci sequence, every element is the sum of the two preceding elements. The sequence starts with 0, 1, 1, 2, 3, 5, 8 …
Here’s how you can find the 15th number of the Fibonacci sequence:
func fibonacci(position: Int) -> Int {
switch position {
// 1
case let n where n <= 1:
return 0
// 2
case 2:
return 1
// 3
case let n:
return fibonacci(position: n - 1) + fibonacci(position: n - 2)
}
}
let fib15 = fibonacci(position: 15) // 377
- If the current sequence position is less than two, the function will return
0. - If the current sequence position equals two, the function will return
1. - Otherwise, the function will use recursion to call itself and sum up all the numbers. This code also avoids the
defaultcase in aswitchstatement. Thelet ncase matches all values, so thedefaultcase is unnecessary.
FizzBuzz
In the FizzBuzz algorithm, your objective is to print the numbers from 1 to 100, except:
- On multiples of three, print
"Fizz"instead of the number. - On multiples of five, print
"Buzz"instead of the number. - On multiples of both three and five, print
"FizzBuzz"instead of the number.
for i in 1...100 {
// 1
switch (i % 3, i % 5) {
// 2
case (0, 0):
print("FizzBuzz", terminator: " ")
case (0, _):
print("Fizz", terminator: " ")
case (_, 0):
print("Buzz", terminator: " ")
// 3
case (_, _):
print(i, terminator: " ")
}
}
print("")
Here’s what’s going on:
- You construct a tuple in the
switchexpression. - Each of the cases checks a result of the modulo operation. The underscore means you don’t care, and it matches any value.
- In this code, you see another equivalent way to avoid writing the
defaultcase of aswitchstatement with a tuple pattern of all underscores(_, _)that match any value. This type of pattern is known in the Swift lexicon as an irrefutable pattern.
The terminator parameter of the print call tells the compiler to end each line with a space character instead of a new line. All the numbers in the algorithm will print on one line in your debug area. The final print("") call adds an empty string with a new line so that any future code will print on a new line.
Now you know how to ace those tricky interview questions in a surprisingly elegant fashion using pattern matching. You can thank me later for your new Swift job!
Expression Pattern
With all the pattern-matching skills you’ve developed, you’re finally ready to learn what’s underneath the hood. The expression pattern is simple but, oh, so powerful.
At the beginning of this chapter, you saw the example tuple pattern (x, 0, 0). You learned that, internally, the tuple is a comma-separated list of patterns. You also learned that the x is an identifier pattern, while the 0’s are examples of the expression pattern. So the tuple’s internal patterns are (identifier, expression, expression).
The expression pattern compares values with the pattern-matching operator, ~=. The match succeeds when a comparison returns true. If the values are the same type, the common == equality operator performs the comparison instead. You learned how to implement Equatable and == for your own types in “Swift Apprentice: Fundamentals - Chapter 17: Protocols”.
When the values aren’t of the same type, or the type doesn’t implement the Equatable protocol, the ~= pattern matching operator is used.
For instance, the compiler uses the ~= operator to check whether an integer value falls within a range. The range isn’t an integer, so the compiler cannot use the == operator. However, you can conceptualize the idea of checking whether an Int is within a range. That’s where the ~= pattern matching operator comes in:
let matched = (1...10 ~= 5) // true
As in the definition of a case condition, the pattern must be on the operator’s left-hand side and the value on the right-hand side of the operator. Here’s what the equivalent case condition looks like:
if case 1...10 = 5 {
print("In the range")
}
This if case statement is functionally equivalent to using the ~= operator in the previous example.
Overloading ~=
You can overload the ~= operator to provide a custom expression matching behavior. You’ll implement a pattern match between an array and an integer to check if the integer is an array element. A value of 2 should match the pattern [0, 1, 2, 3]. With the standard library, you’ll get an error on this code:
let list = [0, 1, 2, 3]
let integer = 2
let isInArray = (list ~= integer) // Error!
if case list = integer { // Error!
print("The integer is in the array")
} else {
print("The integer is not in the array")
}
Sure, you could check if the integer is in the array like this:
let isInList = list.contains(integer) // true
But it would be nice to use pattern matching to check for a match within a switch statement. You can implement the missing pattern matcher with this code:
// 1
func ~=(pattern: [Int], value: Int) -> Bool {
// 2
for i in pattern {
if i == value {
// 3
return true
}
}
// 4
return false
}
Here’s what’s happening:
-
The function takes an array of integers as its
patternparameter and an integer as itsvalueparameter. The function returns aBool. -
In the implementation, a
forloop iterates through each element in the array. -
If the value equals the current array element, the function immediately returns
trueand no more code runs within the function implementation. -
If the
forloop finishes without any matches, the function returnsfalse.
Now that the pattern-matching operator is overloaded, the expression patterns you saw earlier now match correctly with no errors.
let isInArray = (list ~= integer) // true
if case list = integer {
print("The integer is in the array") // Printed!
} else {
print("The integer is not in the array")
}
You are now a pattern-matching ninja! With your mastery of patterns, you’re ready to write clear, concise, readable code.
Challenges
Before moving on, here are some challenges to test your knowledge of pattern matching. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Carded
Given this code, write an if statement that shows an error if the user is not yet 21 years old:
enum FormField {
case firstName(String)
case lastName(String)
case emailAddress(String)
case age(Int)
}
let minimumAge = 21
let submittedAge = FormField.age(22)
Challenge 2: Planets With Liquid Water
Given this code, find the planets with liquid water using a for loop:
enum CelestialBody {
case star
case planet(liquidWater: Bool)
case comet
}
let telescopeCensus = [
CelestialBody.star,
.planet(liquidWater: false),
.planet(liquidWater: true),
.planet(liquidWater: true),
.comet
]
Challenge 3: Find the Year
Given this code, find the albums that were released in 1974 with a for loop:
let queenAlbums = [
("A Night at the Opera", 1974),
("Sheer Heart Attack", 1974),
("Jazz", 1978),
("The Game", 1980)
]
Challenge 4: Where in the World
Given the following code, write a switch statement that will print out whether the monument is located in the northern hemisphere, the southern hemisphere, or on the equator.
let coordinates = (lat: 37.334890, long: -122.009000)
Key Points
- A pattern represents the structure of a value.
- Pattern matching can help you write more readable code than the alternative logical conditions.
- Pattern matching is the only way to extract associated values from enumeration values.
- The
~=operator is used for pattern matching, and you can overload it to add your own pattern matching.