Sun, 19 Jul 2015 #
Tuples in Swift, Advanced Usage and Best Practices
Table of Contents
An older version of this post is also available in 🇨🇳Chinese Thanks to SwiftGG
Tuples are one of Swift's less visible language features. They occupy a small space between Structs and Arrays. In addition, there's no comparable construct in Objective-C (or many other languages). Finally, the usage of tuples in the standard library and in Apple's example code is sparse. One could get the impression that their raison d'être in Swift is pattern matching, but I disgress.
Most tuple explanations concentrate on three tuple use cases (pattern matching, return values, destructuring) and leave it at that. The following guide tries to give a more comprehensive overview of tuples with best practices of when to use them and when not to use them. I'll also try to list those things that you can't do with tuples, to spare you asking about them on stack overflow. Let's dive in.
1 The absolute basics
You probably already know most of this, so I'll keep it concise.
A tuple can combine different types into one. Tuples are value types and even though they look like sequences they aren't sequences, as there's no direct way of looping over the contents. We'll start with a quick primer on how to create and use tuples.
1.1 Creating and Accessing Tuples
// Constructing a simple tuple let tp1 = (2, 3) let tp2 = (2, 3, 4) // Constructing a named tuple let tp3 = (x: 5, y: 3) // Different types let tp4 = (name: "Carl", age: 78, pets: ["Bonny", "Houdon", "Miki"]) // Accessing tuple elements let tp5 = (13, 21) tp5.0 // 13 tp5.1 // 21 let tp6 = (x: 21, y: 33) tp6.x // 21 tp6.y // 33
1.2 Tuples for pattern matching
As already mentioned above, this feels like the strongest use case for tuples.
Swift's switch
statement offers a really powerful yet easy way to define complex conditionals without cluttering up the source code. You can then match for the type, existence, and value of multiple variables in one statement:
// Contrived example // These would be return values from various functions. let age = 23 let job: String? = "Operator" let payload: Any = ["cars": 1]
In the code above, we want to find the persons younger than 30 with a job and a Dictionary payload. Imagine the payload as something from the Objective-C world, it could be a Dictionary or an Array or a Number, awful code somebody else wrote years ago, and you have to interact with it now.
typealias AnyDictionary = Dictionary<AnyHashable, Any> switch (age, job, payload) { case (let age, _?, _ as AnyDictionary) where age < 30: print(age) default: break }
By constructing the switch argument as a tuple (age, job, payload)
we can query for specific or nonspecific attributes of all tuple elements at once. This allows for elaborately constrained conditionals.
1.3 Tuples as return types
Probably the next-best tuple use case. Since tuples can be constructed on the fly, they're a great way to easily return multiple values from a function.
func abc() -> (Int, Int, String) { return (3, 5, "Carl") }
1.4 Tuple Destructuring
Swift took a lot of inspiration from different programming languages, and this is something that Python has been doing for years. While the previous examples mostly showed how to easily get something into a tuple, destructuring is a swifty way of getting something out of a tuple, and in line with the abc
example above, it looks like this:
let (a, b, c) = abc() print(a)
Another example is getting several function calls into one line:
let (a, b, c) = (a(), b(), c())
Or, an easy way to swap two values:
var v1: Int var v2: Int (v1, v2) = (5, 4) (a: v1, b: v2) = (a: v2, b: v1) // swapped: v1 == 4, v2 == 5 (v1, v2) = (5, 4) (a: v1, b: v2) = (b: v1, a: v2) // swapped: v1 == 4, v2 == 5
2 Beyond the basics
2.1 Tuples as anonymous structs
Tuples as well as structs allow you to combine different types into one type:
let user1 = (name: "Carl", age: 40) // vs. struct User { let name: String let age: Int } let user2 = User(name: "Steve", age: 39)
As you can see, these two types are similar, but whereas the tuple exists simply as an instance, the struct requires both a struct declaration and a struct initializer. This similarity can be leveraged whenever you have the need to define a temporary struct inside a function or method. As the Swift docs say:
Tuples are useful for temporary groups of related values. (…) If your data structure is likely to persist beyond a temporary scope, model it as a class or structure (…)
As an example of this, consider the following situation where the return values from several functions first need to be uniquely collected and then inserted:
func zipForUser(userid: String) -> String { return "12124" } func streetForUser(userid: String) -> String { return "Charles Street" } let users = [user1] // Find all unique streets in our userbase var streets: [String: (zip: String, street: String, count: Int)] = [:] for user in users { let zip = zipForUser(userid: user.name) let street = streetForUser(userid: user.name) let key = "\(zip)-\(street)" if let (_, _, count) = streets[key] { streets[key] = (zip, street, count + 1) } else { streets[key] = (zip, street, 1) } } // drawStreetsOnMap(streets.values) for street in streets.values { print(street) }
Here, the tuple is being used as a simple structure for a short-duration use case. Defining a struct would also be possible but not strictly necessary.
Another example would be a class that handles algorithmic data, and you're moving a temporary result from one method to the next one. Defining an extra struct for something only used once (in between two or three methods) may not be required.
// Made up algorithm func calculateInterim(values: [Int]) -> (r: Int, alpha: CGFloat, chi: (CGFloat, CGFloat)) { return (values[0], 2, (4, 8)) } func expandInterim(interim: (r: Int, alpha: CGFloat, chi: (CGFloat, CGFloat))) -> CGFloat { return CGFloat(interim.r) + interim.alpha + interim.chi.0 + interim.chi.1 } print(expandInterim(interim: calculateInterim(values: [1])))
There is, of course, a fine line here. Defining a struct for one instance is overly complex; defining a tuple 4 times instead of one struct is overly complex too. Finding the sweet spot depends on various factors.
2.2 Private State
In addition to the previous example, there are also use cases where using tuples beyond a temporary scope is useful. Following Rich Hickey's "If a tree falls in the woods, does it make a sound?", as long as the scope is private and the tuple's type isn't littered all over the implementation, using tuples to store internal state can be fine.
A simple and contrived example would be storing a static UITableView structure that displays various information from a user profile and contains the key path to the actual value as well as a flag noting whether the value can be edited when tapping on the cell.
let tableViewValues = [ (title: "Age", value: "user.age", editable: true), ("Name", "user.name.combinedName", true), ("Username", "user.name.username", false), ("ProfilePicture", "user.pictures.thumbnail", false)]
The alternative would be to define a struct, but if the data is a purely private implementation detail, a tuple works just as well.
A better example is when you define an object and want to add the ability to add multiple change listeners to your object. Each listener consists of a name and the closure to be called upon any change:
typealias Action = (_ change: Any?) -> Void func addListener(name: String, action: @escaping Action) { } func removeListener(name: String) { }
How will you store these listeners in your object? The obvious solution would be to define a struct, but this is a very limited scope, and the struct will only be internal, and it will be used in only three cases. Here, using a tuple may even be the better solution, as the destructuring makes things simpler:
class ListenerStuff { typealias Action = (_ change: Any?) -> Void var listeners: [(String, Action)] = [] func addListener(name: String, action: @escaping Action) { listeners.append((name, action)) } func removeListener(name: String) { if let idx = listeners.index(where: { $0.0 == name }) { listeners.remove(at: idx) } } func execute(change: Int) { for (_, listener) in listeners { listener(change as Any?) } } } var stuff = ListenerStuff() let ourAction: ListenerStuff.Action = { x in print("Change is \(x ?? "NONE").") } stuff.addListener(name: "xx", action: ourAction) stuff.execute(change: 17)
As you can see in the execute
function, the destructuring abilities make tuples especially useful in this case, as the contents are directly destructured into the local scope.
2.3 Tuples as Fixed-Size Sequences
Another area where tuples can be used is when you intend to constrain a type to a fixed number of items. Imagine an object that calculates various statistics for each month in a year. You need to store a certain Integer value for each month separately. The solution that comes to mind first would of course be:
var monthValuesArray: [Int]
However, in this case we don't know whether the property indeed contains 12 elements. A user of our object could accidentally insert 13 values, or 11. We can't tell the type checker that this is a fixed size array of 12 items1. With a tuple, this specific constraint can easily be put into place:
var monthValues: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)
The alternative would be to have the constraining logic in the object's functionality (say via a guard
statement); however, this would be a run time check. The tuple check happens at compile time; your code won't even compile if you try to give 11 months to your object.
2.4 Tuples for Varargs Types
Varargs i.e. variable function arguments are a very useful technique for situations where the number of function parameters is unknown.
// classic example func sum(of numbers: Int...) -> Int { // add up all numbers with the + operator return numbers.reduce(0, +) } let theSum = sum(of: 1, 2, 5, 7, 9) // 24
A tuple can be useful here if your requirement goes beyond simple integers. Take this function, which does a batch update of n
entities in a database:
func batchUpdate(updates: (String, Int)...) { self.db.begin() for (key, value) in updates { self.db.set(key, value) } self.db.end() } // We're imagining a weird database batchUpdate(updates: ("tk1", 5), ("tk7", 9), ("tk21", 44), ("tk88", 12))
3 Advanced Tuples
3.1 Tuple Iteration
In the above descriptions, I've tried to steer clear of calling tuples sequences or collections because they aren't. Since every element of a tuple can have a different type, there's no type-safe way of looping or mapping over the contents of a tuple. Well, no beautiful one, that is.
Swift does offer limited reflection capabilities, and these allow us to inspect the elements of a tuple and loop over them. The downside is that the type checker has no way to figure out what the type of each element is, and thus everything is typed as Any
. It is your job then to cast and match this against your possible types to figure out what to do.
let t = (a: 5, b: "String", c: Date()) let mirror = Mirror(reflecting: t) for (label, value) in mirror.children { switch value { case is Int: print("int") case is String: print("string") case is NSDate: print("nsdate") default: () } }
This is not as simple as array iteration, but it does work if you really need it.
3.2 Tuples and Generics
There's no Tuple
type available in Swift. If you wonder why that is, think about it: every tuple is a totally different type, depending on the types within it. So instead of defining a generic tuple requirement, you define the specific but generic incarnation of the tuple you intend to use:
func wantsTuple<T1, T2>(_ tuple: (T1, T2)) -> T1 { return tuple.0 } wantsTuple(("a", "b")) // "a" wantsTuple((1, 2)) // 1
You can also use tuples in typealiases
, thus allowing subclasses to fill out your types with details. This looks fairly useless and complicated, but I've already had a use case where I need to do exactly this.
class BaseClass<A,B> { typealias Element = (A, B) func add(_ elm: Element) { print(elm) } } class IntegerClass<B> : BaseClass<Int, B> { } let example = IntegerClass<String>() example.add((5, "")) // Prints (5, "")
3.3 Define a Specific Tuple Type
In many of the earlier examples, we rewrote a certain tuple type like (Int, Int, String)
multiple times. This, of course, is not necessary, as we could define a typealias
for it:
typealias Example = (Int, Int, String) func add(elm: Example) { }
However, if you're using a certain tuple construction so often that you think about adding a typealias for it, you might really be better off defining a struct.
3.4 Tuples as function parameters
Swift 3 removed the tuple splat feature, which used to be described in this section.
If you pass a tuple as a parameter to a function, it always works as you would expect: the tuple is available as an immutable variable in the function.
3.5 Tuples to reorder function parameters
Swift 3 removed the tuple splat feature, which was the basis for tricks discussed in this section.
4 Tuple impossibilities
Finally, we reach the list of some of the things that are impossible to achieve with tuples.
4.1 Tuples as Dictionary Keys
If you'd like to do the following:
let p: [(Int, Int): String]
Then this is not possible, because tuples don't conform to the Hashable protocol. Which is really a bummer, as the example above has a multitude of use cases. There may be a crazy type checker hack to extend tuples of varying arities to the Hashable protocol, but I haven't really looked into that. If you happen to know if this works, feel free to contact me via twitter.
4.2 Tuple Protocol Compliance
Given the following protocol:
protocol PointProtocol { var x: Int { get } var y: Int { get } }
You can't get the type checker to accept the tuple (x: 10, y: 20)
as implementing that protocol.
func addPoint(point: PointProtocol) addPoint(point: (x: 10, y: 20) as PointProtocol) // doesn't work.
The compiler complains, "'(x: Int, y: Int)' is not convertible to 'PointProtocol'; did you mean to use 'as!' to force downcast? (Answer: no.)
5 Addendum
That's it. I probably forgot one or another thing. Things may also be wrong. If you find a factual error, or if there's something else I forgot, feel free to contact me.
6 The code, suitable for use in a playground
import AppKit // * Creating and Accessing Tuples // Constructing a simple tuple let tp1 = (2, 3) let tp2 = (2, 3, 4) // Constructing a named tuple let tp3 = (x: 5, y: 3) // Different types let tp4 = (name: "Carl", age: 78, pets: ["Bonny", "Houdon", "Miki"]) // Accessing tuple elements let tp5 = (13, 21) tp5.0 // 13 tp5.1 // 21 let tp6 = (x: 21, y: 33) tp6.x // 21 tp6.y // 33 // * Tuples for pattern matching // Contrived example // These would be return values from various functions. let age = 23 let job: String? = "Operator" let payload: Any = ["cars": 1] typealias AnyDictionary = Dictionary<AnyHashable, Any> switch (age, job, payload) { case (let age, _?, _ as AnyDictionary) where age < 30: print(age) default: () } // * Tuples as return types func abc() -> (Int, Int, String) { return (3, 5, "Carl") } // * Tuple Destructuring let (a, b, c) = abc() print(a) func f1() -> Int { return 1 } func f2() -> Int { return 2 } func f3() -> Int { return 3 } let (r1, r2, r3) = (f1(), f2(), f3()) var v1: Int var v2: Int (v1, v2) = (5, 4) (a: v1, b: v2) = (a: v2, b: v1) // swapped: v1 == 4, v2 == 5 (v1, v2) = (5, 4) (a: v1, b: v2) = (b: v1, a: v2) // swapped: v1 == 4, v2 == 5 // * Tuples as anonymous structs let user1 = (name: "Carl", age: 40) // vs. struct User { let name: String let age: Int } let user2 = User(name: "Steve", age: 39) func zipForUser(userid: String) -> String { return "12124" } func streetForUser(userid: String) -> String { return "Charles Street" } let users = [user1] // Find all unique streets in our userbase var streets: [String: (zip: String, street: String, count: Int)] = [:] for user in users { let zip = zipForUser(userid: user.name) let street = streetForUser(userid: user.name) let key = "\(zip)-\(street)" if let (_, _, count) = streets[key] { streets[key] = (zip, street, count + 1) } else { streets[key] = (zip, street, 1) } } // drawStreetsOnMap(streets.values) for street in streets.values { print(street) } // Made up algorithm func calculateInterim(values: [Int]) -> (r: Int, alpha: CGFloat, chi: (CGFloat, CGFloat)) { return (values[0], 2, (4, 8)) } func expandInterim(interim: (r: Int, alpha: CGFloat, chi: (CGFloat, CGFloat))) -> CGFloat { return CGFloat(interim.r) + interim.alpha + interim.chi.0 + interim.chi.1 } print(expandInterim(interim: calculateInterim(values: [1]))) // * Private State let tableViewValues = [ (title: "Age", value: "user.age", editable: true), ("Name", "user.name.combinedName", true), ("Username", "user.name.username", false), ("ProfilePicture", "user.pictures.thumbnail", false)] class ListenerStuff { typealias Action = (_ change: Any?) -> Void var listeners: [(String, Action)] = [] func addListener(name: String, action: @escaping Action) { listeners.append((name, action)) } func removeListener(name: String) { if let idx = listeners.index(where: { $0.0 == name }) { listeners.remove(at: idx) } } func execute(change: Int) { for (_, listener) in listeners { listener(change as Any?) } } } var stuff = ListenerStuff() let ourAction: ListenerStuff.Action = { x in print("Change is \(x ?? "NONE").") } stuff.addListener(name: "xx", action: ourAction) stuff.execute(change: 17) // * Tuples as Fixed-Size Sequences var monthValuesArray: [Int] var monthValues: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) // * Tuples for Varargs Types // classic example func sum(of numbers: Int...) -> Int { // add up all numbers with the + operator return numbers.reduce(0, +) } let theSum = sum(of: 1, 2, 5, 7, 9) // 24 print(theSum) func batchUpdate(updates: (String, Int)...) { //self.db.begin() for (key, value) in updates { print("self.db.set(\"\(key)\", \(value))") //self.db.set(key, value) } //self.db.end() } // We're imagining a weird database batchUpdate(updates: ("tk1", 5), ("tk7", 9), ("tk21", 44), ("tk88", 12)) // * Advanced Tuples // ** Tuple Iteration let t = (a: 5, b: "String", c: Date()) let mirror = Mirror(reflecting: t) for (label, value) in mirror.children { switch value { case is Int: print("int") case is String: print("string") case is NSDate: print("nsdate") default: () } } // ** Tuples and Generics func wantsTuple<T1, T2>(_ tuple: (T1, T2)) -> T1 { return tuple.0 } let tr1 = wantsTuple(("a", "b")) // "a" let tr2 = wantsTuple((1, 2)) // 1 class BaseClass<A,B> { typealias Element = (A, B) func add(_ elm: Element) { print(elm) } } class IntegerClass<B> : BaseClass<Int, B> { } let example = IntegerClass<String>() example.add((5, "")) // Prints (5, "") // ** Define a Specific Tuple Type typealias Example = (Int, Int, String) func add(elm: Example) { } // ** Tuples as Dictionary Keys // let p: [(Int, Int): String] // doesn't compile // ** Tuple Protocol Compliance protocol PointProtocol { var x: Int { get } var y: Int { get } } func addPoint(point: PointProtocol) { print(point) } // addPoint(point: (x: 10, y: 20) as PointProtocol) // doesn't work. // The compiler complains, // "'(x: Int, y: Int)' is not convertible to 'PointProtocol'; did you mean to use 'as!' to force downcast?
7 Changes
07/23/2015 Added section on tuples as function parameters
08/06/2015 Updated the Reflection example to the latest Swift beta 4. (It removes the reflect
call)
08/12/2015 Updated the Tuples as function parameters with a couple more examples and more information.
08/13/2015 Fixed a couple of bugs..
10/28/2015 Fixed bugs and added a new section on parameter reordering.
Footnotes:
If you read this far, you should follow me (@terhechte)
on Twitter