The Flow of Control Revisited: Matching with Patterns - ADDITIONAL TOPICS - Understanding Swift Programming: Swift 2 (2015)

Understanding Swift Programming: Swift 2 (2015)

PART 3: ADDITIONAL TOPICS

20. The Flow of Control Revisited: Matching with Patterns

In an earlier chapter (Chapter 3 on “The Flow of Control”) I described the basic conditional statements in Swift: if, if-else, and switch. The if and if-else statements work like those in C and other languages but are a little cleaner. Switch statements in Swift are slightly different than those in C, a little safer and cleaner because you do not need a break statement after each case to prevent falling through to the next case test.

In addition, switch statements have more sophisticated matching capabilities than I have described so far. There are also two other statements, if-case and for-case (new in Swift 2) that make use of these same matching capabilities. In this chapter I will describe how these switch statements that use matching patterns work.

Matching Against Patterns

In conventional if statements, for loops, and switch statements, matching is simple. A test compares a value against another value or an arithmetic or logical expression. Thus:

if x == 5 { print(" x is 5") }

if x > 0 && y == 3 { print("x is greater than zero and y is equal to 3")

}

Swift, however, has more sophisticated matching capabilities, and allows matching against one of a number of patterns that are defined.

Swift also allows the use of where clauses to add an additional constraint to the match.

In addition, Swift allows value binding, which sets a temporary variable or constant to the value that is being matched so that that variable or constant can be used in statements within the case clause.

The patterns used in matching are:

Expression Pattern. This is just the conventional expression (e.g., m==5) that values have previously been matched against.

Tuple pattern. This matches a tuple that contains values against values or expressions that are represented as tuples.

Wildcard (underscore) pattern. The underscore character (“_”) will match anything. It is usually used in matching tuples, where one element of a tuple can be any value, and the case clauses mainly refer to other elements of the tuple.

Enumeration case pattern. This matches a value against the value of an enumeration member (e.g., DayOfTheWeek.Tuesday).

Type-casting patterns. This uses an is, as?, or as! operator to perform a type casting operation before the matching is attempted.

The patterns can be mixed together. Thus, the wildcard pattern is often mixed with the tuple pattern.

Just to avoid confusion: These are matching patterns. They have nothing to do with design patterns, such as the singleton, delegate, and similar patterns.

The Basic Switch Statement Syntax

To refresh your memory, here's the basic syntax for a switch statement that was shown in the earlier chapter:

var name = "George"

switch name {

case "Peter":

print ("The name is Peter")

case "George":

print ("The name is George")

case "Jennie":

print ("The name is Jennie")

default:

print ("No match found")

}

Matching Against Expressions

Matching with Swift patterns can be a little more sophisticated than conventional matching, even with just the expression pattern, since ranges can be used.

The expression to be matched must, however, be the same type as the value in the case statement it is being matched against.

An example of a switch statement matching with an integer against a set of ranges is shown below:

var m = 7

switch m {

case 0...5:

print ("The value is from 0 to 5")

case 6..<8:

print ("The value is 6 or 7")

case 8...12:

print ("The value is from 8 to 12")

default:

print ("The value is something else")

}

Using Tuples and Underscores in Switch Statements

Tuples can be used to match cases in switch statements:

var tup = (45,"Don Drysdale")

switch(tup) {

case (35,"Fernando Valenzuela"):

print("Valuenzuela with 35 home runs")

case (44,"Babe Ruth"):

print("Ruth with 44 home runs")

case (45,"Don Drysdale"):

print("Drysdale with 45 home runs")

default:

print("Can't find anything that matches")

}

We can also use an underscore to indicate that, in some cases, any value in a particular position will match:

var tup = (45,"Don Drysdale")

switch(tup) {

case (_,"Fernando Valenzuela"):

print("F. Valenzuela")

case (_,"Monica Valenzuela"):

print("M. Valenzuela")

case (_,"Babe Ruth"):

print("Ruth")

case (45,"Don Drysdale"):

print("Drysdale with 45 home runs")

default:

print("Can't find anything that matches")

}

In this switch statement we will have a match in the first three cases regardless of the number in the first position of the tuple, while in the fourth case we still require that the first position be 45 for a match.

Matching Against Enumeration Cases

Matching can also be done against the member values of enumerations.

An example is shown below:

let e = DayOfTheWeek.Tuesday

switch e {

case DayOfTheWeek.Sunday:

print ("It must be Sunday")

case DayOfTheWeek.Monday:

print ("It must be Monday")

case DayOfTheWeek.Tuesday:

print ("It must be Tuesday")

case DayOfTheWeek.Wednesday:

print ("It must be Wednesday")

case DayOfTheWeek.Thursday:

print ("It must be Thursday")

case DayOfTheWeek.Friday:

print ("It must be Friday")

case DayOfTheWeek.Saturday:

print ("It must be Saturday")

}

There is no default case because the enumeration member values (all of the possible days of the week) are exhaustive.

Value Binding

It is possible to assign values to variables and constants within a case. This is known as value binding and is similar to what is done in if statements when unwrapping optionals. The following example is just like the first example in this chapter, but with a couple of changes in the first case. First, there is no "Peter" to match against, with let x being substituted. And second, when this case matches, it now prints out "The name is \(x)" rather than "The name is Peter".

What is happening is that because there is a let x value binding instead of a value to match against, the first case will always match. A value binding acts like an underscore character and matches anything. The constant x is now set to the value of name. And then, when the print is executed, the value of x will be printed. So the switch statement prints out "The name is Peter" as before. (The example here uses a constant for x because there is no need for a variable, the value being set only once. Variables can also be used. But note that the scope of the constant or variable is limited to only the case clause where the constant or variable was declared.)

var name = "Peter"

switch name {

case let x:

print ("The name is \(x)")

case "George":

print ("The name is George")

case "Jennie":

print ("The name is Jennie")

default:

print ("No match found")

}

Where Clauses

Replacing the value to be matched with the let x of a value binding means that we no longer have a way to match the case with the original value that the switch statement was attempting to find a match for. We can do this matching, however, with a where clause, as shown below. An expression like where x == "Peter" is added. This means that the code in a case will only be executed if the original value matches what is being tested for. What this means in this particular case is that only the second case will be executed, and what will be printed out is "The name is 2 George".

var name = "George"

switch name {

case let x where x == "Peter":

print ("The name is 1 \(x)")

case let x where x == "George":

print ("The name is 2 \(x)")

case let x where x == "Jennie":

print ("The name is 3 \(x)")

default:

print ("No match found")

}

The examples I've shown for value binding are very simple and don’t necessarily need the complexity shown. Often, however, value binding is used when matching with tuples, where things get a little more complex and it can make more sense to use value bindings and where clauses.

In the value binding example there was actually an overlap across some of the cases, with the first case matching anything. This is legitimate. What will happen is that the first case statement that matches will execute, and the others will be ignored.

If-Case Statements

The capability of matching against patterns that has been described above for switch statements can also be used in the new Swift 2 statement, if-case:

let m = 5

if case 1...6 == m {

print("m is within the range 1 to 6")

}

This is a very simple example. The value of the if-case statement is more often seen with more complex situations. Some programmers have used (with Swift 1) a switch statement with a single case (and the overhead of a default case, since switch statements must be exhaustive) because they wanted to use the more advanced pattern matching capabilities in switch. With Swift 2 they can use the simpler if-case statement instead.

For-Case Loops

Matching against patterns can also allegedly be done with the new Swift 2 statement, for-case.

Unfortunately, this capability does not work using the description in the Apple documentation, as of Swift 7.0 beta 2. Some people have been able to make it work with different syntax, but it is not clear whether Apple will change the compiler or change the documentation, so I haven’t described it here.

When this is resolved I will post a description on the understandingswiftprogramming.com web site.

Hands-On Exercises

Go to the following web address with a Macintosh or Windows PC to do the Hands-On Exercises.

For Chapter 20 exercises, go to

understandingswiftprogramming.com/20