There are dozens of reasons to love Swift tuples. Here are five to get you started.
Tuplizing Declarations
Tuples can initialize a bunch of variables or constants at once. For example, here’s a typical set of variable declarations. This code creates several variables and establishes initial values for each.
var x = "XX" var y = "YY" var z = "ZZ"
When there’s an underlying semantic relationship between these items, consider placing them into a single line with semicolons. Yes, you can turn your nose up at this but a core relationship is emphasized by reordering the declarations into a one-line presentation. Items are declared together because they have some kind of meaning together:
var x = "XX"; var y = "YY"; var z = "ZZ"
There are, of course, drawbacks. This presentation complicates any modifications you may later need to make to this group: whether inserting or removing variables, or changing their initial values. That’s a bad thing so reserve this approach to items where there is a long-standing logical reason to group.
In fact, you only want to join declarations when there’s some really compelling structural reason these items are grouped together, and you never want to do this with unrelated items. For example, this next snippet is an abomination against Cthulhu:
var myVariable = initialValue; var theta = 0.0; let radius = 6; var userName = "Bill"
Avoid this bad example: you are a good person and a good coder, and don’t want Cthulhu to swallow your soul.
Instead, consider a case when there’s a true uniformity of intent. When such exists, you can ditch the semicolons and re-design your declarations to use a tuple, as in the following line of code.
var (w, x, y, z) = ("WW", "XX", "YY", "ZZ")
This line produces the same symbols as the four separate statement snippets you saw previously, but it does so using a simple parsimonious approach. If w
, x
, y
, and z
are related, why shouldn’t their declarations be related too?
Tuple declarations become slightly more complicated when the compiler cannot immediately infer typing. In such cases, add explicit typing. For example, 5.0 and 9.6 normally default to double values. You can force these to CGFloat
like this:
var (px, py) = (5.0, 9.6) as (CGFloat, CGFloat) // or var (px, py): (CGFloat, CGFloat) = (5.0, 9.6)
Even with underlying relationships, avoid complicated declarations that use mixed types. These force you to cognitively match each variable or constant with a type and initial value. This next line is an example of what you don’t want to do:
var (a, b, c) : (Int, Double, CGFloat) = (1, 2, 3)
Don’t force yourself to match a variable from column 1 with a type from column 2 and a value from column 3. It’s bad coding, it’s bad neurocognitive overloading, and Cthulhu doesn’t need that many souls.
Becoming the Tuple
Tuples do more than simplify individual declarations. When looking for targets of tuple opportunity, it’s easy to find instances like these:
var gen1 = seq1.generate(); var gen2 = seq2.generate() let item1 = gen1.next(); let item2 = gen2.next()
and transform them into these:
var (gen1, gen2) = (seq1.generate(), seq2.generate()) let (item1, item2) = (gen1.next(), gen2.next())
But in doing so, you’re missing a big opportunity. Use direct tuple declarations in place of separate declaration proxies. Consider this instead:
var generators = (seq1.generate(), seq2.generate()) let items = (generators.0.next(), generators.1.next()) // thanks Steve
Binding related components into a single tuple offers some great programming wins. Here’s a real-world example that benefits from tuple amalgamation. Here’s the original code before conversion.
func longZip<S1: SequenceType, S2: SequenceType> (seq1: S1, _ seq2: S2) -> AnyGenerator<(S1.Generator.Element?, S2.Generator.Element?)> { var (gen1, gen2) = (seq1.generate(), seq2.generate()) return anyGenerator { let (item1, item2) = (gen1.next(), gen2.next()) guard item1 != nil || item2 != nil else {return nil} return (item1, item2) } }
In this example, both the generator and item assignments would benefit from tuplification.
There’s also a nagging syntactic issue to consider before conversion. Tuple field numbering starts at 0, not 1. So items.0 is relates to gen1
, and items.1
to gen2
. This can be ugly if you’re using 1-based numbering. You’ll want to change this when performing your conversion.
Here’s what you get after tuplifying and re-numbering the variables and type parameters:
func longZip<S0: SequenceType, S1: SequenceType> (seq0: S0, _ seq1: S1) -> AnyGenerator<(S0.Generator.Element?, S1.Generator.Element?)> { var generators = (seq0.generate(), seq1.generate()) return anyGenerator { let items = (generators.0.next(), generators.1.next()) guard items.0 != nil || items.1 != nil else {return nil} return items } }
Notice how much nicer the return statement is as well.
Adjusting field names
Tuples are basically anonymous structs. As you’ve seen, you get numbered field names for free:
let myTuple = (1, "Hello", 5.2)
gives you
(.0 1, .1 "Hello", .2 5.2)
with each field automatically typed and numbered.
Numbers only take you so far. You can manually add field names to declarations for richer semantics, as in the following example. As you see, “intItem” carries more meaning than “.0”
You can also turn this into a typealias, which you can use to pick up field names:
Unfortunately, this type alias is tied to specific types. You cannot create generic typealiases that refer to structure this way:
typealias PairType<T, U> = (first: T, second: U) // won't work.
That’s a pity. A “Pair” is semantically rich but inherently generic. The answer, at this time, is to work around this with Any
. The Any
type fixes the declaration issue even if it’s aesthetically displeasing:
typealias PairTuple = (first: Any, second: Any) let bar: PairTuple = (2, "hello") print(bar.first.dynamicType) // Int.Type print(bar.second.dynamicType) // String.Type
If you insist on using generics, Mike Ash, tongue in cheek, offers the following alternative:
struct Pa<T, U> { typealias ir = (first: T, second: U) } let foo = Pa.ir(2, "Hello") print(foo.first.dynamicType) // Int.Type print(foo.second.dynamicType) // String.Type
Unfortunately, this beautifully named struct does not work with the original mission statement, despite its otherwise sterling qualities:
let gar: Pa = (2, "Hello") // Error!
Swapping values
When it comes to value swaps, you cannot beat the tuple shuffle. Simply construct tuples out of the items whose values you want to reassign, and use tuple-assignment to move those values around. Here’s an example:
(x, y) = (y, x)
You’re not limited to pairs. You can use this approach with as many variables as required:
(x, y, z, w) = (w, x, y, z)
When executed in optimized code, 2-tuple value swaps are as efficient as the built-in swap
function, and n-tuple value swaps are as efficient as tmp=first, first=second, second=third,...,last=tmp
swaps. It’s a simple, beautiful approach.
Testing for Nil
Bless pattern matching for it is good. Remember this line from the example at the top of the post?
guard items.1 != nil || items.2 != nil else {return nil}
This code tests to see whether both sequences have been exhausted, returning nil from the combined sequences if all items have been used from both incoming generators. Checking multiple items against nil is a pretty common Swift task, and it’s one that really benefits from tuples.
In this example, there’s a far better approach than looking individually at each tuple field. Pattern matching enables you to leverage an if-case
statement to determine whether a tuple contains a .Some
case or not. Here’s what that test looks like:
if case (.None, .None) = items {return nil}
The if-case here “binds” the tuple field items against the tuple in the case. It uses a single equal sign even though there is no actual assignment here. When items is (nil, nil)
, the generator returns nil.
This statement is kind of an odd bird. The condition looks like you could use it as a boolean intermediate for, for example, a ternary condition:
return case (.None, None) = items ? nil : items // nope! won't work
But this is not legal Swift and will not compile. An if-case performs pattern matching binding and exposes any newly bound symbols in its scope clause. This tuple-test uses a byproduct of binding to add early exit but it’s a bit off-label if you get my meaning.
Part 6 of 5: Better Return Values
Sebastian Celis writes, “My personal favorite way to use tuples is when a function really wants to return more than one piece of data. I prefer the tuple to being forced to use an inout parameter”
He’s right. There is never a good reason for you to return multiple distinct values with in-out parameters. Use proper error handling if needed and return a tuple instead.
func status() -> (code: Int, text: String) { // RFC 2324 return (418, "I'm a teapot") }
In-out side-effects? Not even once.
Wrap-Up
So there you have it. Five really cool ways to use tuples, and this write-up doesn’t even begin to touch on the ways you can use tuples in switch statements.
Tuples are one of the big paradigm-shift areas in Swift (along with algebraic data types, pattern matching, value types, protocol oriented programming, functional programming, etc) but tuples tend to get short shrift in terms of people paying proper attention to them.
Hopefully this post will give them a little of the love they deserve and raise your awareness of their awesomeness and possibilities. Remember: Swift isn’t just about re-writing code using new syntax. It’s about embracing new ways of thinking about and architecting your code.
One Comment
Nice summary! One error:
var generators = (seq1.generate(), seq2.generate())
let items = (gen1.next(), gen2.next())
should be:
var generators = (seq1.generate(), seq2.generate())
let items = (generators.0.next(), generators.1.next())
Correct?