Tuple Destructuring in computed properties

Destructuring tuples is a very common way to improve the use of Tuples. It does not work with computed properties.

Take this code for example:

var hourMinute: (Int, Int) {
    let date = Date()
    let cal = Calendar.current
    let hour = cal.component(.hour, from: date)
    let minute = cal.component(.minute, from: date)
    
    return (hour, minute)
}

print(hourMinute.0) 

First of all I had to name this tuple hourMinute which is honestly not the greatest name. Then I had to access the value by using the index of the item in the tuple. I can infer from the name hourMinute that hour is 0, and minute 1.

Of course I can improve the ambiguity by labeling:
var hourMinute: (hour: Int, minute: Int)
print(hourMinute.hour)

However destructuring is a really great feature that allows you to pull the tuple apart into multiple variables all within a single assignment.

let (hour, minute) = hourMinute
print(hour)

The problem here is that I needed to create a local destructured version to my computed property: hourMinute

So naturally, I'd like to destructure the computed property. This works out really nicely. I don't need to make a hodgepodge name hourMinute

Instead I could define it a destructured tuple right from the get go.

var (hour, minute): (Int, Int) {
    let date = Date()
    let cal = Calendar.current
    let hour = cal.component(.hour, from: date)
    let minute = cal.component(.minute, from: date)
    
    return (hour, minute)
}

This does not work. The compiler throws Getter/setter can only be defined for a single variable

In my opinion this is not ideal because you'd expect it work. But it does not.

Learners of Swift are going to pickup destructuring perhaps from a tutorial and then attempt something like this and become confused. "Oh I guess that doesn't work."

3 Likes

Note, that the hourMinute getter will be called every time you use it:

// app starts around 17:59
print(hourMinute.hour)    // 17 (current time 17:59:59:99999999)
print(hourMinute.minute)  // 00 (current time 18:00:00:00000001)
// unexpected result: 17 00

As to why "Getter/setter can only be defined for a single variable" error - no idea.

4 Likes

I think “called every time” is why. If you need both values, you’ll end up running the code twice; if you just need one, you’ll still compute both and then throw one away. At that point it’s good to be a little more explicit about it. A matter of opinion, though, to be sure.

6 Likes

Good point about the date example. Probably not the finest one to use.

1 Like

Technically doesn't calling hourMinute.hour also call the whole thing and dispose of the other?

Sure. I'd do something like this instead:

let (hour, minute) = Calendar.current.time // equivalent to your hourMinute
print(hour)
print(minute)

I sympathise with @bryan here. It's pretty common that I end up with ill-conjoined properties like this because there's essentially a single calculation that derives both, so of course you don't want to duplicate that code between two computed properties - even though that's a cleaner API from the outside.

One can certainly have a private func doTheActualDerivation() -> (Int, Int) and put two properties over it as the actual API, but of course then you're guaranteeing wasted time for callers that do use both properties.

You can memoise etc, but that's quite a bit of extra work (especially since Swift still lacks an equivalent to Python's @memoize decorator), and has its own downsides regarding cache lifetimes and memory use etc. In a lot of cases the compiler can tell what properties are actually accessed and with what lifetimes, so it can in theory do better.

I don't know what the solution to this could be, in terms of a language enhancement, but I concur there's a desire for it. I run into it a lot when wrapping network APIs (where folks often choose to bundle together disparate data because of some historical performance problem, but it's otherwise cleaner to present the data as independent).

Having a way to explicitly declare a function (or computed property) as pure would be a start, at least. In combination with inlining that might happen to eliminate redundant work. One would prefer stronger guarantees than "might", though. And a more general solution that works for impure functions.

1 Like

I’m not sure the syntax makes sense tbh :face_with_peeking_eye:

It feels semantically conflicting to me how you declare a variable and define a type.

Correct me if I’m wrong, but this approach declares two variables “hour” and “minute” while using the tupel-type-definition syntax?

let myTupel: (Int, Int) = (1, 2)

var hour: Int = 1
var minute: Int = 2

Then in the proposal you are creating hour of type Int and minute of type Int but use the tupel syntax (Int, Int) to do that?

var (hour, minute): (Int, Int) = (1, 2)

It's called tuple destructuring. Consider a simpler example:

func somethingThatReturnsTuple() -> (Int, Int) {
    /* return */ (1, 2)
}
let (x, y) = somethingThatReturnsTuple()
let (x, y): (Int, Int) = somethingThatReturnsTuple() // same

in this case x and y will be set to 1 and 2 respectively.

It's the same in your example, just because there's constant (1, 2) it looks more ridiculous and hardly a win compared to, say:

let hour = 1, minute = 2

The topic's question is why do we not allow this:

var (x, y): Int {
    /* return */ (1, 2)
}

which could have been equivalent to:

var __temp: (Int, Int) {
    /* return */ (1, 2)
}

var x: Int {
    /* return */ __temp.0
}

var y: Int {
    /* return */ __temp.1
}
1 Like