Tuples in Swift, Advanced Usage and Best Practices
A brief introduction into Swift's tuples including a basic introduction and several useful tips for advanced usage.
Introduction
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 we 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.
The absolute Basics
A tuple can combine different types into one. Where an array is a sequence
of a certain type (let x: [Int] = [1, 2, 3, 4, 5]
) a tuple can have
a different type for each element: let x: (Int, Double, String) = (5, 2.0, "Hey")
.
Tuples are a very simple manner of grouping related data items together without having to create a struct.
They are value types and even though they look like sequences they aren't. One main difference is that you can't easily loop over the contents of a tuple.
We'll start with a quick primer on how to create and use tuples.
Creating and Accessing Tuples
// Constructing a simple tuple
let tuple1 = (2, 3)
let tuple2 = (2, 3, 4)
// Constructing a named tuple
let tupl3 = (x: 5, y: 3)
// Different types
let tuple4 = (name: \"Carl\", age: 78, pets: [\"Bonny\", \"Houdon\", \"Miki\"])
Once you've created some tuples, you can access their elements:
// Accessing tuple elements
let tuple5 = (13, 21)
tuple5.0 // 13
tuple5.1 // 21
// Access by name
let tuple6 = (x: 21, y: 33)
tuple6.x // 21
tuple6.y // 33
Nice, so now you can create tuples and access their elements. But what would you
use them for? The use case we will discuss is for pattern matching
.
Tuples for Pattern Matching
Pattern matching oftentimes 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.
We wrote a game where both players have to take a quiz. After each question we evaluate who won or lost. There're four states:
- Player 1 is correct
- Player 2 is correct
- Both were correct
- Both were wrong
We can model this logic very nicely with a switch
statement such as the following:
let player1 = true
let player2 = false
switch (player1, player2) {
case (true, false): print(\"Player 1 won\")
case (false, true): print(\"Player 2 won\")
case (true, true): print(\"Draw, both won\")
case (false, false): print(\"Draw, both lost\")
}
Here, we create a tuple (player1, player2)
and then match each of
the possible cases.
Much more complex matchings are also possible. Below, we will match over
a tuple that has the type (Int, String?, Any)
.
// Contrived example
// These would be return values from various functions.
let age: Int = 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, weakly typed code that somebody else wrote years ago, and you have to interact with it now.
switch (age, job, payload) {
case (let age, _?, _ as Dictionary<AnyHashable, Any>) 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.
Tuples as Return Types
Probably the next-best tuple use case is using them to return one-time structures. Tuples as return types makes sense when you need to return multiple return types.
The following function is required to return a User
from our database.
However, in order to speed up processing, the user may be loaded from the
cache instead of a query. If that's the case, the caller of the function
should also receive the information that the User
was cached, and how
long it was since the last update from the database. We can easily do this
with tuples:
func userFromDatabase(id: Int) -> (user: User, cached: Bool, updated: Date) {
...
}
This function returns three values, the actual user user
, whether the user is
cached cached
and when the user was lasted updated
.
This also saves you from introducing a new struct
type.
Since tuples can also be deconstructed on the fly, this even allows re-introducing the variables at the call site:
let (user, cached, lastUpdated) = userFromDatabase()
This will create three new variables in the current scope: user
, cached
, and lastUpdated
.
We will describe this destructuring
in more detail in the next chapter.
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:
func abc() -> (Int, Int, Int) {
return (1, 2, 3)
}
let (a, b, c) = abc()
print(a) // prints 1
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
Advanced Tuples
Now that we've seen the basics of how to use tuples, we will look at more advanced use cases. This also means there will be less often situations where you can apply those patterns.
Nevertheless, it is still very valuable to have them in your toolbox.
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.
Private State
In addition to the previous example, there are also use cases where using tuples beyond a temporary scope is useful. 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.
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 items. 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.
Variable Arguments
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()
}
batchUpdate(updates: (\"tk1\", 5),
(\"tk7\", 9),
(\"tk21\", 44),
(\"tk88\", 12))
This pattern allows us to model the insertions in a very simple manner without having to introduce additional struct types.
Iteration
So far, 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.
You can also abstract this into a nice function which translate your tuple type into a parseable description.
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, \"\")
You can also define a typealias
with generic parameters
like in this example where we introduce a custom Either
type:
typealias MyEither<A, B> = (left: A, right: B)
Type Aliases
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.
Similar Articles |
---|