Swift Fundamentals

Tuples

Earlier in this series, we covered arrays, sets, and dictionaries. Collections are ideal for storing objects of the same type. Remember that Swift is very strict about type safety. That's why you're not allowed to store a string in an array of integers.

Swift Type Safety

But there are times that you want to group objects that are of different type. Let me show you what I mean with an example. Let's assume you want to store the name of a file, its mime type, and its size. One option is to create a type for this purpose, a structure for example.

struct FileData {

    let name: String
    let type: String
    let size: Int

}

This example illustrates how easy that is in Swift. Don't worry about the syntax. We cover structures in more detail later in Swift Fundamentals.

Creating an instance of the FileData structure is intuitive and concise.

let file = FileData(name: "draft.md", type: "text/markdown", size: 2076)

This is a fine solution, especially for an application that works with files. Sometimes, however, you don't want to define a dedicated type for trivial problems like this. If a separate type is too much, then a tuple is the next best option.

What Is a Tuple

A tuple groups multiple values into a single compound value. The following example illustrates this concept.

let file = ("draft.md", "text/markdown", 2076)

The file constant is of type (String, String, Int). Tuples can combine multiple values of multiple types. That's what makes them powerful. There are several approaches to access the values stored in a tuple. The easiest approach is to use an index, which may remind you of arrays.

file.0
file.1
file.2

What I like most about tuples is a concept referred to as decomposition. In this example, we bind or map the values of a tuple to a number of constants.

let (name, type, size) = file

print(name)
print(type)
print(size)

There's another option to access the values of a tuple. Instead of using indices, you can use names to access the values stored in a tuple. By naming each of the values of the tuple, we can access the values of the tuple by name instead of by index. If you're working with complex tuples, naming the values adds clarity and avoids confusion.

let file = (name: "draft.md", type: "text/markdown", size: 2076)

print(file.name)
print(file.type)
print(file.size)

When to Use a Tuple

Developers new to Swift often wonder why tuples are part of the Swift language. When should you use a tuple instead of a collection type? What problems do tuples solve?

When working with tuples, it's important to remember that tuples are designed for temporarily storing related data. For more complex data structures, it's better, and even recommended, to make use of classes and structures.

Tuples work very well with functions. They're ideal as return types of functions. Why is that? I already mentioned that a tuple can store values of a different type. Take a look at this example in which we define a function that returns the details of a file.

func fetchFileDetails() -> [String:Any] {
    // ...

    return [ "name": "draft.md", "type": "text/markdown", "size": 2076 ]
}

This example is obviously contrived, but it highlights a limitation of collection types. The function returns a dictionary with keys of type String and values of type Any. Any is a bit of an odd type. As the name implies, Any can represent a value of any type. That's why we can store strings and integers in the dictionary we return from the fetchFileDetails() function. Don't worry about this for now. Remember that this episode is about tuples.

While it may seem that there's nothing wrong with the fetchFileDetails() function, it isn't perfect, far from it. What happens if we return the size of the file as a string?

func fetchFileDetails() -> [String:Any] {
    // ...

    return [ "name": "draft.md", "type": "text/markdown", "size": "2076" ]
}

Nothing happens and that's exactly the problem. This may sound odd. Let me illustrate how tuples can improve this example. The fetchFileDetails() function should return three attributes of the file, the name, the type, and the size. Each value needs to be of a specific type. We can define these requirements in a tuple.

func fetchFileDetails() -> (String, String, Int) {
    ...
}

We cover functions in detail later in Swift Fundamentals. Remember for now that the return type of a function is defined after the arrow and before the opening curly brace. By defining the return value of the function with a tuple instead of a dictionary, we have no other choice but to conform to the requirements of the tuple. Notice that the compiler shows us an error.

Invalid Return Type

Let's update the implementation of the fetchFileDetails() function to resolve the error.

func fetchFileDetails() -> (String, String, Int) {
    // ...

    return ("draft.md", "text/markdown", 2076)
}

Because we defined the type of the tuple, we know exactly what the fetchFileDetails() function returns. Using the fetchFileDetails() function becomes easy and transparent.

let fileDetails = fetchFileDetails()

fileDetails.0
fileDetails.1
fileDetails.2

We can go one step further and name the values of the tuple in the function definition.

func fetchFileDetails() -> (name: String, type: String, size: Int) {
    // ...

    return ("draft.md", "text/markdown", 2076)
}

let fileDetails = fetchFileDetails()

fileDetails.name
fileDetails.type
fileDetails.size

And, if we add decomposition to the mix, the result is an elegant piece of Swift code.

let (name, type, size) = fetchFileDetails()

Take Advantage of the Compiler

But there's more to the story. Because tuples are strict about the values they can store, the compiler helps us detect bugs early. Let's illustrate this with another example. We want to show the user the file size in kilobytes. We divide the file's size and use the result to create a string.

func fetchFileDetails() -> (name: String, type: String, size: Int) {
    // ...

    return ("draft.md", "text/markdown", 2076)
}

let (name, type, size) = fetchFileDetails()

let sizeAsKB = size / 1024
let sizeAsString = "\(sizeAsKB)KB"

One of your colleagues decides to change the API of the fetchFileDetails() function and decides to return the file's size as a string. Even if your colleague doesn't notify you of this change, the compiler spots the change and throws an error.

Compiler Error

The compiler sees the return type of the fetchFileDetails() function has changed and it's no longer possible to divide the file's size to calculate the number of kilobytes. This example shows how a strongly typed language helps detect potential bugs early.

Give It a Try

Tuples may feel foreign at first and you may not know what to do with them. That's common. If someone hands you a tool you're not familiar with, then it may not be immediately obvious why you'd need it. Give tuples a try and see how they can improve the code you write.

Next Episode "Conditionals"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By