Swift 2: Control Flow Pattern Matching Examples
This article aims to provide examples of the expanded pattern matching support which ships with Swift 2.
Pattern matching using if
and guard
New in Swift 2 is support for pattern matching within if
statements (and guard
statements). Let’s first define a simple enum:
// A generic 'number' type that can represent either an integer, a
// floating-point value, or a boolean.
enum Number {
case IntegerValue(Int)
case DoubleValue(Double)
case BooleanValue(Bool)
}
Checking for a certain case
Use case: we want to check whether a value corresponds to a certain case. This works regardless of whether the case has associated values or not, but does not unbox the values (if they exist).
let myNumber : Number = // ...
if case .IntegerValue = myNumber {
print("myNumber is an integer")
}
The pattern, case .IntegerValue
, comes first, and the value to be matched, the variable myNumber
, comes after the equals sign. This might seem counterintuitive, but it’s similar to how the optional-unwrapping if let a = a
is written: the value that is being checked comes after the equals sign.
The equivalent Swift 1.2 version, using switch
, follows:
switch myNumber {
case .IntegerValue: print ("myNumber is an integer")
default: break
}
Unboxing an associated value
Use case: we want to check whether a value corresponds to a certain case, and if so we want to extract the associated value(s).
if case let .IntegerValue(theInt) = myNumber {
print("myNumber is the integer \(theInt)")
}
The pattern now becomes case let .IntegerValue(theInt)
. The value to be matched remains the same as in the previous example.
Here is an example of the same concept, but applied to guard
. The semantics of the predicates for guard
and if
are identical, and so pattern matching works exactly the same way.
func getObjectInArray<T>(array: [T], atIndex index: Number) -> T? {
guard case let .IntegerValue(idx) = index else {
print("This method only accepts integer arguments!")
return nil
}
return array[idx]
}
Qualifying with where
clauses
As with any case
in a switch
statement, a where
clause can optionally follow a pattern to provide an additional constraint. Let’s modify the getObjectInArray:atIndex:
function from the previous example:
func getObjectInArray<T>(array: [T], atIndex index: Number) -> T? {
guard case let .IntegerValue(idx) = index where idx >= 0 && idx < array.count else {
print("This method only accepts integer arguments that are in bounds!")
return nil
}
return array[idx]
}
Complex if
predicates
Swift’s if
statement is surprisingly capable. An if
statement can have multiple predicates, separated by commas. Predicates fall into one of three categories:
- Simple logical test (e.g.
foo == 10 || bar > baz
). There can only be one such predicate, and it must be placed first. - Optional unwrapping (e.g.
let foo = maybeFoo where foo > 10
). If an optional unwrapping predicate immediately follows another optional unwrapping predicate, thelet
can be omitted. Can be fitted with awhere
qualifier. - Pattern matching (e.g.
case let .Bar(something) = theValue
), as discussed above. Can be fitted with awhere
qualifier.
Predicates are evaluated in the order they are defined, and no predicates after a failing predicate will be evaluated. Here is a (contrived) example of a complicated if
statement.
let a : Int = // ...
let b : Int = // ...
let firstValue : Number? = // ...
let secondValue : Number? = // ...
if a != b,
let firstValue = firstValue,
secondValue = secondValue,
case let .IntegerValue(first) = firstValue,
case let .IntegerValue(second) = secondValue where second > first {
print("a and b are different, and secondValue is greater than firstValue")
print("a + b + firstValue + secondValue = \(a + b + first + second)")
}
As always, prioritize intelligibility over cleverness when building such constructs.
Pattern matching using for
-in
Pattern matching can be used in conjunction with the for
-in
loop. In this case the intent is to iterate over the items within a sequence, but only those items which match the given pattern. An example follows:
enum EngineeringField : String {
case Civil, Mechanical, Electrical, Chemical, Nuclear
}
enum Entity {
case EngineeringStudent(name: String, year: Int, dept: EngineeringField)
case HumanitiesStudent(name: String, year: Int, dept: String)
case EngineeringProf(name: String, dept: EngineeringField)
case HumanitiesProf(name: String, dept: String)
}
// A list of entities currently present in the classroom.
var currentlyPresent = [
Entity.EngineeringProf(name: "Alice", dept: .Mechanical),
Entity.EngineeringStudent(name: "Belinda", year: 2016, dept: .Mechanical),
Entity.EngineeringStudent(name: "Charlie", year: 2017, dept: .Chemical),
Entity.HumanitiesStudent(name: "David", year: 2017, dept: "English Literature"),
Entity.HumanitiesStudent(name: "Evelyn", year: 2018, dept: "Philosophy"),
Entity.EngineeringStudent(name: "Farhad", year: 2017, dept: .Mechanical)
]
// Only iterate over the engineering students
for case let .EngineeringStudent(name, _, dept) in currentlyPresent {
print("\(name), who studies \(dept) Engineering, is present.")
}
/* Output:
Belinda, who studies Mechanical Engineering, is present.
Charlie, who studies Chemical Engineering, is present.
Farhad, who studies Mechanical Engineering, is present.
*/
In the for
-in
predicate, the pattern each element is compared against precedes the in
keyword, and the sequence to iterate over follows.
Note that, as with patterns in switch
statements, we can unwrap multiple associated values, and ignore those we don’t care about using _
. We could, if necessary, also add a where
clause with an additional condition to further constrain which elements we iterate over.
Pattern matching using while
Pattern matching can also be used in conjunction with the while
loop. In this case the intent is to repeatedly run the loop body so long as some value in the predicate successfully matches a pattern. To wit:
enum Status {
case Continue(Int)
case Finished
}
func doSomething(input: Int) -> Status {
if input > 5 {
return .Finished
} else {
// Do work
print("Doing work with input \(input)")
return .Continue(input + 1)
}
}
var latestStatus = doSomething(1)
while case let .Continue(nextInput) = latestStatus {
latestStatus = doSomething(nextInput)
}
/* Output:
Doing work with input 1
Doing work with input 2
Doing work with input 3
Doing work with input 4
Doing work with input 5
*/
Note that the compound predicates described above in Complex if predicates are also supported by the while
loop, including use of where
clauses.
Advanced patterns
A brief overview of some of the other patterns supported by the pattern matching mechanism follows.
Optional pattern
The new optional pattern, myValue?
, is equivalent to .Some(myValue)
for optionals. (Note that you can use these patterns within a switch
statement’s case
as well, like with all other patterns.)
// Refer to the 'Number' enum from before.
let myValue : Number? = // ...
if case .IntegerValue? = myValue {
print("myValue was not nil, it is some unspecified integer")
}
if case let .IntegerValue(theInt)? = myValue {
print("myValue was not nil, and it contained the integer \(theInt)")
}
Nested enumerations
As before, patterns can be nested, including enumeration patterns. The previous examples could also be written as:
if case .Some(.IntegerValue) = myValue {
print("myValue was not nil, it was some unspecified integer")
}
if case let .Some(.IntegerValue(theInt)) = myValue {
print("myValue was not nil, and it contained the integer \(theInt)")
}
Type checking patterns
The type checking patterns is
and as
can be used to determine:
- Is an instance of the protocol type
T
actually an instance of the concrete typeU
(whereU
conforms toT
)? - Is an instance of the class
T
actually an instance of the classU
(whereU
is a subclass ofT
)?
class Animal {
var age : Int
init(age: Int) { self.age = age }
}
class Cat : Animal {
var hasFur : Bool, name : String
init(age: Int, name: String, hasFur: Bool) {
self.hasFur = hasFur
self.name = name
super.init(age: age)
}
}
class Dog : Animal { /* ... */ }
let myPet : Animal = // ...
// 'is' if we only care about whether the downcast succeeded
if case is Dog = myPet {
print("myPet is a Dog.")
}
// 'as' is we want a reference to the value as the downcasted type
if case let myCat as Cat = myPet {
print("myPet is a Cat named \(myCat.name) who is \(hasFur ? "not" : "") a Sphynx cat")
}
Unsurprisingly, we can nest type checking patterns within other patterns, such as the enumeration patterns described earlier.
enum Thing {
case SomeAnimal(Animal), SomeVegetable, SomeMineral
}
let anObject : Thing = // ...
if case .SomeAnimal(is Cat) = anObject {
print("anObject is a SomeAnimal containing a Cat")
}
if case let .SomeAnimal(theCat as Cat) = anObject {
print("anObject contains a \(theCat.age)-year-old Cat named \(theCat.name)")
}
Other patterns
At this point it should hopefully be clear how the other patterns Swift supports can be utilized in conjunction with the control flow statements. For example, Swift supports matching on ranges:
let myNumber = 50
if case 0..<100 = myNumber {
print("myNumber is between 0 and 99")
}
It also supports matching on expression patterns (the behavior of which defaults to comparison by ==
, and can be customized by overriding ~=
):
let protagonist = "jyaku"
if case "jyaku" = protagonist {
print("you could have used a plain old 'if' statement for this")
}
Theoretically, you should be able to match tuple patterns, but (as of Xcode 7.1β2) the compiler segfaults instead:
let a = true
let b = true
if case (true, true) = (a, b) {
print("both 'a' and 'b' are true")
}
To switch
or not to switch
?
Before Swift 2, pattern matching showed up in only two places: multiple variable assignment, and the switch
statement. If any of an enumeration’s cases contained associated values, switch
was the only way to manipulate instances of that enum. The exception to this was Optional
, for which the special if let
syntax was devised.
Swift 2 grants to all enums the power of if let
, and much more. Pattern matching and control flow logic work together, meaning that one no longer needs to (for example) write for
-in
loops whose bodies consist of a single switch
statement, or alternate nesting if
and switch
statements.
Pattern matching and control flow also obviate the need for a number of patterns. For example, it was sometimes desirable to add ‘accessor’ properties to enums to retrieve the associated values for specific cases:
extension Number {
var valueAsInteger : Int? {
switch self {
case let .IntegerValue(value): return value
default: return nil
}
}
var valueAsBoolean : Bool? {
switch self {
case let .BooleanValue(value): return value
default: return nil
}
}
// ...
}
This way, you could use if let
instead of switch
if one really only cared about whether an enum instance was one specific case or not:
func getObjectInArray<T>(array: [T], atIndex index: Number) -> T? {
switch index {
case let .IntegerValue(index): return array[index]
default: return nil
}
}
// became...
func getObjectInArray<T>(array: [T], atIndex index: Number) -> T? {
if let index = index.valueAsInteger {
return array[index]
}
return nil
}
With Swift 2, such boilerplate is no longer necessary.
However, switch
statements still have their uses, the most important of which is probably exhaustiveness checking. For enumerations with more than three cases, switch
statements still provide the ability to check that all cases were covered, something that if
, for
-in
, and while
cannot.
This is helpful if you later go back and add cases to your enum; the compiler will flag every place you switch against that enum (unless you were lazy and used default
or case _
needlessly). In a large project where an enum may be switched against in hundreds or thousands of places, this feature can mean the difference between introducing undiscoverable bugs when refactoring or not.
Thanks to @gsimmons for catching a typo in the sample code.