Why does this Tuple assignment work?

I've been thinking about this over and over.

Sometimes, I think I get it, but then I lose it and I struggle to see how this assignment works.

var a = 1
var b = 2
(a, b) = (b, a)
print(a, b)
// Outputs 2 1

I first saw this in a Python discussion about Tuples, and it was until then just a simple value exchange problem. You have a and b, but you want a to become b and b to become a.

The Tuple allows this in a single expression, whereas usually you would use a temporary value:

var a = 1
var b = 2
let temp = a
a = b
b = temp

But the great thing with the tuple approach is we can eliminate the temporary variable and the need for 3 lines of code and just do:

(a, b) = (b, a)

So when I encountered Swift and found the same behaviour to be true I was initially happy. But when asked to explain what was happening, I began to realise I truly didn't know.

Well, perhaps I should clarify. The fact that it worked meant I understood what was happening, but I couldn't really explain why it was like that.

"A tuple is a lightweight collection type".

So what does:

(a, b) = (b, a)

actually mean when a Tuple is a lightweight collection?

The Value of b is placed into the value container a, and correspondingly the value of a is placed into the value container b. This is what does actually happen.

But it seems to break a lot of what is known about Swift.

If a tuple is a value type, then the a and b within the tuples would be copies, so the subsequent print of the original variables would print their original values, not the swapped values. So Tuples clearly are not value types.

This is in line with the definition that Tuples are "lightweight collection types". So far, so good,

So what does:

(a, b) = (b, a)

actually mean to a lightweight collection?

Surely it must mean that the collection (a, b) is replaced by the collection (b, a).

But if that was actually the case, a would still print as 1 and b would still print as 2. But somehow, a has been assigned the value of b.

What is the somehow that did that?

You’re using a feature called destructuring using a tuple pattern. There’s a section of The Swift Programming Language devoted to this.

When you write (b, a) on the right hand side of the assignment operator, you’re creating a new tuple value which is equal to (2, 1). It would be exactly equivalent for you just to write (2, 1).

When you use a destructuring assignment—here, (a, b) = (2, 1), you are telling Swift to assign the first element of the tuple on the right hand side to the first variable in the tuple pattern on the left hand side, and so forth. It would be exactly equivalent to writing a = 2; b = 1.

5 Likes

Alternatively, swap(&a, &b)

As for the part about tuples, xwu’s answer is correct. The RHS is an independent tuple with its own copy of b and a, which you assign to a and b, respectively.

EDIT: FWIW, while tuple-swapping and the swap free function are semantically equivalent, the free function is optimised to avoid retains/releases. You won't see the difference for a pair of Ints, but you will if you use a class:

class MyClass {}

func doTest(a: inout MyClass, b: inout MyClass) {
  (a, b) = (b, a)
}

compiles to:

        push    rbp
        mov     rbp, rsp
        push    rbx
        push    rax
        mov     rax, qword ptr [rsi]
        mov     rcx, qword ptr [rdi]
        mov     qword ptr [rdi], rax
        mov     rbx, qword ptr [rsi]
        mov     qword ptr [rsi], rcx
        mov     rdi, rax
        call    swift_retain@PLT <---- 😱
        mov     rdi, rbx
        add     rsp, 8
        pop     rbx
        pop     rbp
        jmp     swift_release@PLT

While the free function doesn't retain or release anything:

func doTest(a: inout MyClass, b: inout MyClass) {
  swap(&a, &b)
}
        push    rbp
        mov     rbp, rsp
        mov     rax, qword ptr [rdi]
        mov     rcx, qword ptr [rsi]
        mov     qword ptr [rdi], rcx
        mov     qword ptr [rsi], rax
        pop     rbp
        ret

This is really a micro-optimisation though, which I think comes from the fact that the items are not allowed to alias (whereas (a, b) = (b, b) is totally allowed).

2 Likes

Thanks. Exactly what I needed. Much appreciated

Interesting. Thanks.

It was mainly the assignment part that was messing with my brain. Without understanding that destructuring was happening, I couldn't explain the result.