The Tuple Pattern

The Tuple Pattern
Photo by Priscilla Du Preez / Unsplash

In Swift, a tuple is a parenthesized list of two or more elements of any type. For example, (0.5, "foo") is a tuple where the first element is a Double, and the second is a String. One of the key benefits tuples provide is that they make it possible to write a method or function that returns more than one value.

A common use case for this is enumerating a dictionary in a for-in loop. The loop first obtains an iterator object from the dictionary, and then calls the iterator's next() method each time through the loop to obtain the current key and value as a tuple. Here's an example:

let dogInfo: [String: Any] = ["name": "Fido", "breed": "Lab", "age": 5]

for (key, value) in dogInfo {
    print("\(key): \(value)")
}
// Prints
// name: Fido
// breed: Lab
// age: 5

Understanding the Tuple Pattern

Tuples can be used in several different contexts, one being an expression value. So, for example, you can use a tuple as an initializer:

let temperature = (72, "Fahrenheit")
print(temperature)   // Prints "(72, "Fahrenheit")"

You can then access individual elements by position:

print(temperature.0) // Prints "72"
print(temperature.1) // Prints "Fahrenheit"

You can also use a tuple in a type declaration:

var temperature: (Double, String)

Intuitively then, you might think that tuple is a type, but it's not --- it's a pattern. So, what's a pattern? Well, here's how pattern is defined in The Swift Programming Language (3.1 Edition):

A pattern represents the structure of a single value or a composite value. For example, the structure of a tuple (1, 2) is a comma-separated list of two elements. Because patterns represent the structure of a value rather than any one particular value, you can match them with a variety of values. For instance, the pattern (x, y) matches the tuple (1, 2) and any other two-element tuple. In addition to matching a pattern with a value, you can extract part or all of a composite value and bind each part to a constant or variable name.

Okay, that's a mouthful, so perhaps walking through some examples can add a bit of clarity. To begin with, a tuple doesn't define an object with properties or behaviors. Instead, a tuple describes the internal structure of a single, compound value. For example, the following line binds a variable, item to a tuple of data types:

var item: (Double, Int)

So item's data type is an ordered list of two types. This means that when we assign a value to item, it must be a grouping of two individual values, the first a Double, and the second an Int:

item  = (19.99, 2)

Binding Individual Elements

You can use the tuple pattern to define variables and constants. The following line defines two constants, x and y, whose types are both inferred to be Int:

let (x, y) = (10, 20)

which is conceptually similar to:

let x = 10, y = 20

However, in the former case, the tuple pattern has the effect of decomposing the individual values of the tuple on the right, before binding them to the constants on the left.

Note that the same pattern (x, y), can be used to define constants that differ not only by value, but also by type. In the following definition, x is a Double, and y is a String.

let (x, y) = (0.5, "foo")

Accessing Tuple Elements

A tuple's elements can be referenced by position:

let item  = (19.99, 2)

print("price: \(item.0), quantity: \(item.1)")

// Prints "price: 19.99, quantity: 2"

In addition, you can label a tuple's elements using the same syntax you'd use to define a function's parameter names (tuples are intentionally similar to parameter lists), and then access individual values by name:

let item = (price: 19.99, quantity: 2)

print("price: \(item.price), quantity: \(item.quantity)")

// Prints "price: 19.99, quantity: 2"

Again, these identifiers aren't analogous to object properties or keys in a dictionary; they simply provide the compiler with labels for individual components of a compound value.

Another way to access the elements of a tuple of values is to bind them to individual variables or constants. For example, suppose we're working with a class that has a computed property that returns a tuple:

// Defines a computed property with two named elements,
// 'price' and 'quantity', that returns (Double, Int).
//
var defaultItem: (price: Double, quantity: Int) {
    return (19.99, 2)
}

Then in the body of an instance method, we could use the tuple pattern to define a pair of let constants, and bind the values of the individual elements of the tuple as follows:

// Defines individual let constants, 'amount' and 'number'.
// Compiler infers their types from the type of 'defaultItem`.
//
let (amount, number) = defaultItem

print("price: \(amount), quantity: \(number)")
// Prints "price: 19.99, quantity: 2"

The Enumeration Case and Value Binding Patterns

The Swift case keyword makes it possible to combine two additional Swift patterns, enumeration case, and value-binding. Here's what the documentation): says about them:

Enumeration Case Pattern

An enumeration case pattern matches a case of an existing enumeration type. Enumeration case patterns appear in switch statement case labels and in the case conditions of if, while, guard, and for-in statements.

Value-Binding Pattern

A value-binding pattern binds matched values to variable or constant names. Value-binding patterns that bind a matched value to the name of a constant begin with the let keyword; those that bind to the name of variable begin with the var keyword.

The code in the following example loops over an array of tuples, using an if case construct to test whether the quantity of the current item is 2, and if so, binding the tuple's price (first element) to  amount.

// An array whose elements are tuples representing price and quantity
let items = [(12.99, 2), (14.95, 3), (19.99, 2)]
for item in items {
    // Binds price value to 'amount'
    // Enters the 'if' statement's body if quantity matches the pattern '2'
    if case (let amount, 2) = item {
        print(amount)
    }
}
// Prints 
// "12.99"
// "19.99"

// Note that the let keyword can be moved outside the tuple
for item in items {
    if case let (amount, 2) = item {
        print(amount)
    }
    ...

You may be more familiar with the use of the case keyword in switch statements. The following example is similar to the preceding one in that it shows combined use of the enumeration case and value-binding patterns, but this time in the context of a switch statement:

let discount1 = 10.0
let discount2 = 14.0

let items = [("Shirt", 44.99), ("Shoes", 89.99), ("Jeans", 64.99)]

for item in items {
    // Applies a $10 discount for shirts and a $14 discount for shoes by
    // pattern matching on the string value of the first element
    switch item {
    case let ("Shirt", p):  print("Shirt: $\(p - discount1)")
    case let ("Shoes", p):  print("Shoes: $\(p - discount2)")
    case let (itemName, p): print("\(itemName): $\(p)")
    }
}
// Prints
// Shirt: $34.99
// Shoes: $75.99
// Jeans: $64.99

Back to the Future

Let's revisit the example from the beginning of this post, which led off by defining a dictionary as follows:

let dogInfo: [String: Any] = ["name": "Fido", "breed": "Lab", "age": 5]

The example then proceeded to enumerate the dictionary's keys and values with a for-in loop:

for (key, value) in dogInfo {
    print("\(key): \(value)")
}

From what we've already learned about the enumeration case pattern, we can now correctly infer that there's an implicit case-let after the keyword for:

// Valid Swift, but `case let` will be inferred by the compiler if omitted.
for case let (key, value) in dogInfo {
    print("\(key): \(value)")
}

The earlier example also noted that our for-in loop obtains an iterator (an instance of DictionaryIterator) from the dictionary, and calls the iterator's next() method each time through the loop to obtain a tuple of the current key and value. Here's the declaration of the next() method from the Swift Library documentation:

mutating func next() -> (key: Key, value: Value)?

As you can see, next() returns an optional, generically typed tuple with named elements. (Note: we'll explore optional values and the Optional type in detail in a future blog post.) So we now know that the for-in loop could also have been written like so (though generally, the earlier style is preferable):

for item in dogInfo {
    print("\(item.key): \(item.value)")
}
// Prints
// name: Fido
// breed: Lab
// age: 5

Here we simply capture the current tuple in item, and then access item's elements by name in the argument to the print() function.

Conclusion

Some of the concepts embodied in Swift may seem unintuitive to those of us coming from primarily object-oriented language backgrounds. A solid understanding of Swift patterns can help a lot with comprehension when reading non-trivial code, and can be indispensible when struggling to figure out how to make use of various features of the language to implement details of the apps we're writing.