Swift Development: Nobody Knows the Tuples I've Seen

Ron 10/24/2016 0
In Swift, as in many other modern languages, finite ordered lists called tuples group items together into single units. The Swift version of tuples appears in parentheses as a sequence of comma-separated elements.Tuples may look a bit like arrays, but a tuple creates a syntactically distinct construct. What's the difference between the two? A tuple is a fixed-length vector containing varying types; an array is an ordered collection of data that shares a unifying type.

Tuples as Structs

Tuples are essentially anonymous structures. Like standard structs, tuples allow you to combine types in an addressable fashion. A tuple of (1, "Hello", 2.4) is more or less equivalent to the following struct instance:
struct MyStruct {
    var a: Int
    var b: String
    var c: Double
}
let mystruct = MyStruct(a: 1, b: "Hello", c: 2.4)
But wait, you say. That (1, "Hello", 2.4) tuple doesn't have names, and the struct does. You access tuple fields with .0, .1, .2, and the struct fields with .a, .b, .c.In fact, Swift allows you to add labels to tuples. For example, (a:1, b:"Hello", c:2.4) is a perfectly acceptable tuple:
let labeledTuple = (a:1, b:"Hello", c:2.2)
labeledTuple.1 // "Hello"
labeledTuple.b // "Hello"
When a tuple is defined using labels, you access it both by ordered fields (.0, .1, .2) and label fields (.a, .b, .c). The opposite is not true. You cannot refer to a structure by field order.It's a pity that Swift doesn't offer a grand unified structure/tuple/class type. These things seem more alike than different to me, and simpler universal concepts would add nicely to the language's elegance. However, people familiar with Swift assure me that there are compelling reasons this has not happened.

From Tuples to Structs

You cannot easily convert a struct to a tuple, but you can (sort of) convert a tuple to a struct:
let labeledTuple = (a:1, b:"Hello", c:2.2)
let mystruct = MyStruct(labeledTuple)
This example compiles and runs, but you're not actually creating a struct from the tuple. Instead, you're using the tuple as arguments to the struct's initializer. Tuples can be passed to nearly any argument list, for use as initializers, functions, methods, closures, and so forth.Because of the way structs create default initializers, you can't build this struct with an unlabeled tuple, such as (1, "Hello", 2.2). You must first add an initializer that doesn't require labels. The following example creates that second initializer:
struct MyStruct {
    var a: Int
    var b: String
    var c: Double
    init (a: Int, b: String, c: Double) {
        self.a = a; self.b = b; self.c = c
    }
    init (_ a : Int, _ b : String, _ c : Double) {
        self.init(a:a, b:b, c:c)
    }
}

let labeledTuple = (a:1, b:"Hello", c:2.2)
let mystruct = MyStruct(labeledTuple)

let unlabeledTuple = (1, "Hello", 2.2)
let mystruct2 = MyStruct(unlabeledTuple)
After you add initializers that match both labeled and unlabeled elements, you can construct structs from either kind of tuple.

Tuple Limitations

Swift tuples include significant limitations. You cannot programmatically create tuples ("Build me a tuple of length n"). Tuples cannot have mutable length, and there are no variadic tuples (although you can add variadic elements within tuples).What's more, there's no such thing as a 1-tuple. It's a "nope-le," or, more accurately, a scalar. In Swift,foo and (foo) are identical. In theory, Swift has a labeled 1-tuple; for example, (a:42), but this also currently acts as a simple scalar:
let x = (a:42)
x // 42
x == 42 // true
x == (42) // true
x == (a:42) // true
// x.a  // error 'Int' does not have a member named 'a'
Swift reflection lets you count the number of fields within a type. The counts returned for these examples are instructive:
var a = (1, 6, 4)
var b = (2,3)
var c = (2)
var d = ()

reflect(a).count // 3
reflect(b).count // 2
reflect(c).count // 0, scalar, not a tuple, Int 2
reflect(d).count // 0, Void

Tuple Types

Whether or not a tuple uses labels affects the tuple's type. An unlabeled tuple is simply an ordered list of types, whereas a tuple with field names uses those labels as part of its type:
// Type (Int, String, Double)
let unlabeledTuple = (1, "Hello", 2.2)
// Type (a: Int, b: String, c: Double)
let labeledTuple = (a:1, b:"Hello", c:2.2)
With this label rule, the following type alias:
typealias LabeledTupleType = (a:Int, b:String, c:Double)
is not equivalent to this type alias:
typealias UnlabeledTupleType = (Int, String, Double)
because the latter doesn't include labels. This difference is most important when you use tuples as argument lists for closures, functions, and methods.

Assigning Tuples

Here are some examples of assigning tuples to variables:
let params1 = (1, "Params1", 2.4)
let params2 : UnlabeledTupleType = (1, "Params1", 2.4)
let params3 : LabeledTupleType = (1, "Params3", 2.4)
let params4 : LabeledTupleType = (a:1, b:"Params4", c:2.4)
let params5 = (a:1, b:"Params5", c:2.4)
// let params6 : UnlabeledTupleType = (a:1, b:"Params6", c:2.4) // error
// let params7 : LabeledTupleType = (x:1, y:"Params7", z:2.4) // error
The first two items (params1, params2) create identical anonymous tuples. They are essentially identical and interchangeable.The next three (params3, params4, params5) create a similar set of labeled tuples. Swift type inferencing enables the construction of params3 even though it doesn't use labels on the right side of the assignment.The final two assignments are illegal. You cannot assign labels to a type where they aren't defined, or mismatch labels when they are.You can play assignment tango with tuples, letting the labels, parameters, and argument types dance around:
let v : (a: Int, b:Int) = (1, 2)
let v = (a:1, b:2)
let v : (a: Int, b:Int) = (a:1, b:2)
let v : (a: Int, b:Int) = (b:2, a:1)

Passing Tuples as Arguments

The following closure uses the same signature as all the tuples seen so far, namely (Int, String, Double):
var myclosure = {(a:Int, b:String, c:Double)->() in println("\(a) \(b) \(c)")}
You can call this closure with myclosure(2, "Hello", 5.2). You can also call it with both params1and params2, which are the unlabeled tuples:
myclosure(2, "Hello", 5.2)
myclosure(params1)
myclosure(params2)
// myclosure(params3) // error
Swift errors if you pass a labeled tuple to myclosure. The tuple type (a:Int, b:String, c:Double)doesn't match the closure's expected parameters (Int, String, Double), even though that type is identical to the one used to construct the closure.Here's where it gets a little stranger. You can rework the closure to accept a typed tuple using theLabeledTupleType type alias:
var myclosure2:LabeledTupleType->() = {println("\($0.a) \($0.b) \($0.c)")}
With this approach, the closure works for both labeled and unlabeled parameter types:
for tuple in [params1, params2, params3, params4, params5] {myclosure2(tuple)} // works!
Swift uses type inferencing to enable unlabeled tuples to parameterize this closure, even though each field is addressed with the labels a, b, and c.There are consequences when changing closure typing like this. You cannot call myclosure2 using normal arguments. You need to tupleize your arguments as in the following examples:
// myclosure2(5, "Anonymous", 9.8) // does not work
// myclosure2(a:5, b:"Labeled", c:9.8) // does not work
myclosure2((a:5, b:"ParamTuple", c:9.8)) // works
myclosure2((5, "AnonymousTuple", 9.8)) // works
As before, you can use anonymous or labeled tuples, but the tuple labels must match those defined in the LabeledTupleType type.If you want to get especially funky, you can mix up the parameter order, and it still works:
myclosure2((c:5.2, b:"Hello", a:6))
Neat, huh?

Functions

Here is a function similar to the previous closures. It expects three arguments using the same Int,String, and Double types:
func myfunction(a: Int, b:String, c:Double){println("\(a) \(b) \(c)")}
You can call this function using myfunction(5, "Ciao", 9.8) but not myfunction(a:5, b:"Bye", c:9.8). The Swift compiler complains about extraneous argument labels in the latter example because functions default to unlabeled parameters.Unlike with closures, you can adjust function (and method) parameters to force or omit labels from their calling signature. To mandate labels for each parameter, add a hash mark before each label:
func myfunction2(#a: Int, #b:String, #c:Double){println("\(a) \(b) \(c)")}
The first of these two functions (myfunction) works with all the parameter tuples previously defined, including both anonymous and labeled varieties:
for tuple in [params1, params2, params3, params4, params5] {myfunction(tuple)}
This is not a universal solution; it won't work with tuples whose labels don't match the declared labels exactly:
let otherTuple = (x: 5, y:"Hello", z:6.6)
myfunction(otherTuple) // does not work
Once you add mandatory labels, as in myfunction2, unlabeled tuples stop working. Only those tuples whose types contain labels are compatible:
// for tuple in [params1, params2] {myfunction2(tuple)} // does not work
for tuple in [params3, params4, params5] {myfunction2(tuple)} // works
The tuple labels must also be in order. You cannot call myfunction2 with (c:99.0, a:2, b:"Robin").

Tuple Return Types

Functions, methods, and closures may return tuples. Tuples provide an excellent way to group related information together as an informal anonymous struct:
func MyReturnTuple() -> (a:Int, b:String, c:Double) {
    return (1, "Hello", 2.2)
}
MyReturnTuple() returns a labeled tuple, whose fields you can access with either .0, .1, and .2 or.a, .b, and .c.For a more meaningful example, a web service method might return an HTTP status code tuple such as (404, "Page not Found"):
func fetchWebStatus(url : NSURL) -> (Int, String) {
    // …function code here…
    return (418, "I'm a Teapot (see RFC 2324)")
}
You decompose tuples by assignment:
let returnValues = fetchWebStatus() // returns tuple
let (statusCode, statusMessage) = returnValues // breaks tuple into components
When you're only interested in some of the tuple values, use the underscore (_) wildcard expression to skip specific assignments. To fetch the status message by position, instead of saying this:
let statusMessage = returnValues.1
use this:
let (_, statusMessage) = returnValues

Functions with Tuple Arguments

Function declarations allow you to use tuple types. The tuple acts as an intermediate argument, so it lacks the elegance of a standard function:
func myfunction3(tuple:UnlabeledTupleType){println("\(tuple.0) \(tuple.1) \(tuple.2)")}
func myfunction4(tuple:LabeledTupleType){println("\(tuple.a) \(tuple.b) \(tuple.c)")}
You can call both versions with all five example parameters:
For tuple in [params1, params2, params3, params4, params5] {myfunction3(tuple)}
for tuple in [params1, params2, params3, params4, params5] {myfunction4(tuple)}
When working with tupled arguments, as in the following examples, add parentheses to create explicit tuples:
// myfunction3(5, "X", 2.2) // does not work
// myfunction4(a:5, b:"X", c:2.2) // does not work
myfunction3((5, "X", 2.2)) // works
myfunction4((a:5, b:"X", c:2.2)) // works
You improve readability by performing a tuple assignment within the function, but the extra work defeats the point of using a tuple type:
Func myfunction5(tuple : LabeledTupleType) {
    let (a, b, c) = tuple
    println("\(a) \(b) \(c)")}

Geek Tuple Overview

For the sake of obsessive semi-completeness, Table 1 summarizes whether arguments will or will not compile over a range of function signatures. The named tuple variables are assigned as follows:
let unlabeledTuple = (2, 3)
let labeledTuple = (a:2, b:3)

Table 1. A Geek Tuple Challenge Chart

Example(2,3)((2, 3))unlabeledTuplelabeledTuplefunc fa(Int, Int) {}WorksFailsWorksFailsfunc fb((Int, Int)) {}FailsWorksWorksWorksfunc fc(_ : (Int, Int)) {}FailsWorksWorksWorksfunc fd(a : Int, b : Int) {}WorksFailsWorksFailsfunc fe(#a : Int, #b : Int) {}FailsFailsFailsWorksfunc ff((a : Int, b : Int)) {}FailsWorksWorksWorks

Wrap-Up

Swift is a language in flux. How tuples do and don't coerce each other with regard to labels is also in flux. The details may not be intentional in terms of the current compiler implementation. Certainly, the correlation between named structs and tuples are not as fleshed out as they could be.However consistently or inconsistently Swift treats tuples, this article showcases the first-class role they currently play—and will continue to play—in Swift development. Whether working with closure and function arguments or with anonymous structures, tuples remain a key component in Swift whose importance will likely grow in future language updates.