|

Tuple Patterns: Destructure Values Where You Use Them

Molecules of Swift, episode #002

A tuple keeps a few values together. But sometimes the next piece of code needs those values separately.

Take this small response:

let response = (status: 200, message: "OK")

We can read each value through response.status and response.message. Or we can take the tuple apart:

let (status, message) = response

Now we have two local values: status and message.

This is what tuple patterns are good at. Keep the values together when that is useful. Take them apart when separate names make the code easier to read.

Match the tuple

The code on the left has to match the tuple on the right:

let (status, message) = response

response has two values, so the pattern also needs two places. This does not compile:

// let (status, message, extra) = response

Swift checks this at compile time. A tuple pattern must match the number of values in the tuple.

Ignore what you do not need

Suppose we have:

let user = (id: 42, name: "Mira", isAdmin: true)

If we only need the ID and the admin flag, we can use _ for the name:

let (id, _, isAdmin) = user

_ means: there is a value here, but we do not need it.

This is better than creating a variable such as unusedName. The code shows right away which values we want to use.

Take tuples apart in a loop

This also works well with an array of tuples:

let scores = [
    (name: "Mira", score: 91),
    (name: "Noah", score: 84),
    (name: "Ava", score: 97)
]

for (name, score) in scores {
    print("\(name): \(score)")
}

We could also write:

for item in scores {
    print("\(item.name): \(item.score)")
}

Both are fine. Choose the version that fits the code inside the loop.

If the loop works with name and score separately, taking the tuple apart in the for line keeps the rest of the code simple. If the loop still treats the value as one item, keeping item can be clearer.

Match several values in a switch

Tuple patterns are also useful in switch:

let point = (x: 0, y: 4)

switch point {
case (0, 0):
    print("origin")
case (0, _):
    print("y-axis")
case (_, 0):
    print("x-axis")
case (-5...5, -5...5):
    print("inside the box")
default:
    print("outside")
}

Each case checks both values at once.

(0, 0) matches only the origin. (0, _) checks the first value and ignores the second. (-5...5, -5...5) checks whether both values are inside the given ranges.

Order matters. Swift uses the first case that matches, so (0, 0) comes before the more general (0, _).

Match one value and name another

A pattern can check one part of a tuple and create a local name for another:

let point = (x: 8, y: 0)

switch point {
case (let x, 0):
    print("x-axis at \(x)")
case (0, let y):
    print("y-axis at \(y)")
case let (x, y):
    print("elsewhere at \(x), \(y)")
}

In the first case, y must be 0. If it matches, Swift puts the first value into x.

There is also a small Swift shortcut here:

case let (x, y):

When we want to name every value, we can put one let before the whole pattern.

Add a where condition

Sometimes matching the tuple is not enough. We also want to compare its values:

let vector = (x: 3, y: -3)

switch vector {
case let (x, y) where x == y:
    print("main diagonal")
case let (x, y) where x == -y:
    print("opposite diagonal")
case let (x, y):
    print("ordinary vector: \(x), \(y)")
}

The pattern first gives us x and y. The where part then adds one more check.

This can keep a small condition close to the case where it belongs. If the condition gets long or hard to read, move it into a named value or helper instead.

Poof! Let’s rewind...

In #001 — Tuples: Group Values Without Creating a Type, we used a simple rule: a tuple works well for a few local values. If those values start to represent one clear concept in the program, a named type can be a better choice. Tuple patterns do not change that rule. They only make tuples easier to use.

When tuple patterns are useful

Tuple patterns are a good fit when you get a small tuple and immediately need its values separately. They are also useful in loops and in switch cases that check several related values together.

But there is no need to take every tuple apart.

let (id, name, isAdmin) = user

Use this when the next code really needs id, name, and isAdmin as separate values.

If the code still works with the user as one thing, user.name may be easier to follow.

The same idea works for bigger tuples. If you keep passing the same large tuple around and taking it apart in many places, it may be time to create a type for it.

The goal is not to save a few characters. The goal is to make the code easier to understand.

Play with it

The companion Molecule #002 has runnable examples for destructuring, _, for-in, switch, ranges, value binding, where, and if case.

The last compiler example is commented out on purpose. Uncomment it and change the number of values in the pattern. Swift will show an error because the pattern no longer matches the tuple.

Keep tuples together until you need the parts

A tuple is useful when a few values belong together for a while. A tuple pattern is useful when the next code needs those values separately.

A simple rule works well: keep the tuple while you need the whole value. Take it apart when separate names make the next code clearer.

Sources