Redeclaration of named tuple members

Again, I can‘t test it right now because I‘m on my phone. I meant something like this, but I‘m not sure if it will compile.

let final: (min: Int, sec: Int) = counter.quotientAndRemainder(dividingBy: 60) as (Int, Int)
1 Like

Ah, I see. This actually works (in one line). Clever. Thank you. Nice Idea.^^ However, it still has this explicit name erasing intermediate step. Plus you have to practically double the type annotation, which becomes really annoying if it's not just a tuple of two. ;-)

Now, this 'solution' brings up quite a strange warning in my code, although it's working. I do this in a guard let assignment.

guard let time: (min: Int, sec: Int) = someOptional?.quotientAndRemainder(dividingBy: 60) as? (Int, Int)

This gives:

Conditional downcast from '(quotient: Int, remainder: Int)?' to '(Int, Int)' is equivalent to an implicit conversion to an optional '(Int, Int)'

Replace ' as? (Int, Int)' with ''

I'm told to remove the cast completely. That would be my original code, which is not working. Funny guy, this compiler. ;) as and as! don't work either. Perhaps some complicated setting of brackets around… Stop! That doesn't make the code better readable.

I guess there is room for improvement on the language side.

Can you try if the example I previously wrote also works if you remove the last (Int, Int) and move the type you want to the end?

let final = something.method() as (x: Int, y: Int)

Something like this? Not sure if it compiles either. ;)

In case of the optinal you may try as? (x: Int, y: Int) directly?

I used these types because i can type them faster on the phone. Use the correct types instead.

1 Like

Nope. Doesn't compile.
Thanks for talking your time on this matter, BTW!^^

Tuple element labels (or lack of labels) are part of the tuple's type. Conversion is allowed between labeled and unlabeled tuples (with matching element types):

func test() {
    var t = (1, 2)
    var t1 = (a: 1, b: 2)
    var t2 = (c: 3, d: 4)
    
    print(type(of: t)) // (Int, Int)
    print(type(of: t1)) // (a: Int, b: Int)
    print(type(of: t2)) // (c: Int, d: Int)
    t = t1 // OK
    t = t2 // OK
    t1 = t // OK
    t2 = t // OK
    //t1 = t2 // ERROR: Cannot assign value of type '(c: Int, d: Int)' to type '(a: Int, b: Int)'
    //t2 = t1 // ERROR: Cannot assign value of type '(a: Int, b: Int)' to type '(c: Int, d: Int)'
    t1 = t2 as (Int, Int) // OK
    t2 = t1 as (Int, Int) // OK
}
test()

Some subtleties:

func test() {
    var t1 = (x: 1, 2)
    var t2 = (3, x: 4)
    
    print(type(of: t1)) // (x: Int, Int)
    print(type(of: t2)) // (Int, x: Int)
    
    t1 = t2 // OK
    print(t1) // (x: 4, 3)
    t2 = t1 // OK
    print(t2) // (3, x: 4)
}
test()

func test() {
    var t1 = (1, x: 2)
    var t2 = (x: 3, x: 4) // OK (perhaps surprisingly enough for being a bug: https://bugs.swift.org/browse/SR-8974)
    
    print(type(of: t1)) // (Int, x: Int)
    print(type(of: t2)) // (x: Int, x: Int)
    
    t2 = t1 // OK
    print(t2) // (x: 2, x: 1)
    t1 = t2 // OK
    print(t1) // (1, x: 2)
}
test()

Do you think this is justified, desirable?

I think it is somehow more desirable than what you ask for (blindly allow conversion between any tuples with matching element types disregarding their labels) because it would remove the use of / need for labels, at least from a type safety point of view.

I forgot to mention another subtle side of the current behavior:

func test() {
    var a = (x: 1, y: 2)
    var b = (y: 3, x: 4)
    
    a = b // OK (perhaps surprisingly)
    print(a) // (x: 4, y: 3)
    b = a // OK (perhaps surprisingly)
    print(b) // (y: 3, x: 4)
    
    print(a == b) // false (perhaps surprising that two values of different type can be checked for equality?)
    print(a == b as (x: Int, y: Int)) // true
    print(type(of: a) == type(of: b)) // false
}
test()

I don't think the above is desirable behavior and it will not compile if tuple shuffling is deprecated. But if what you ask for would be allowed, then I guess the above would have to compile, and the result would probably be different, I guess it would be like this:

// NOTE: Speculated alternative behavior if label-ignoring conversion was allowed:
func test() {
    var a = (x: 1, y: 2)
    var b = (y: 3, x: 4)
    
    a = b // OK
    print(a) // (x: 3, y: 4)
    b = a // OK
    print(b) // (y: 3, x: 4)
    
    print(a == b) // true
    print(a == b as (x: Int, y: Int)) // true
    print(type(of: a) == type(of: b)) // true ... I guess ... ?
}
test()

Which would arguably be even stranger than the current behavior. Also, would the type of a and b be different or the same?

I think the current behavior + deprecated tuple shuffles makes most sense, all aspects (that I can think of) taken into consideration.

2 Likes

Wouldn't that be 'just' a matter of scope, like in shadowing variables?

// so valuable function ;)
func foo(value: Int?) {
    if let value = value {
        print(value) // unwrapped local value
    }
    print(value) // original passed in optional value is still alive
}

Tuple elements can always be accessed by their index. The label, as I see it, is just a nice to have convenience to ease access and readability. Why shouldn't it be possible to shadow the labels or convert them, if I can do that in an intermediate label erasing step anyway? o_O
If the label really matters that much, what is the difference from a struct? The quotientAndRemainder(dividingBy:)function that gave me the intro could 'equally' well return a struct.

I find it rather strange that you annotate the status quo code three times with 'surprisingly' or 'surprising' but still thinks that the code where you don't find anything 'surprisingly' is still 'stranger'… ;)

How about the following, which is currently allowed, and will continue to be allowed even after tuple shuffling is deprected, and is caused by the == operator that accepts any two tuples with matching element types and ignores their labels:

func test() {
    let ten = 10
    let rq = (remainder: 3, quotient: 1)
    let qr = ten.quotientAndRemainder(dividingBy: 3)
    print(qr == rq) // true <--- What? So 3 * 1 + 3 == 10 ... !?
    print(type(of: rq) == type(of: qr)) // false
}
test()

IMHO it shouldn't even be OK to, without explicit intent, check two values of different types for equality like this.

For me they are not different types, but two tuples containing two Ints. They only differ in the backyard by naming the elements.

let rq = (yellow: 3, blue: 1)
let qr = ten.quotientAndRemainder(dividingBy: 3)
print(qr == rq) // true <--- What? So 3 * 1 + 3 == 10 ... !?

Wait! Now, yellow and blue have the wrong quotient and remainder? It's a naming issue of variables.

If you explicitlly ask for these names, you get these names. That is my inclination…

The labels are meant to carry meaning, yellow and blue become less problematic (and more nonsensical) than remainder and quotient in reverse order. I think neither should compile and I've made my position clear and motivated it thoroughly above and would only repeat myself if I continued this debate : ).

I think you're not seeing tuples as types, but more of a convenient bundling of independent variables. To go back to structs, you wouldn't expect the following to work:

struct S { let x: Int, y: Int }
struct T { let x: Int, y: Int }

let s: S = T() // error: wrong type

The two structs not only have the same members, in the same order, with the same types, they are also the same names. Yet, S and T are considered distinct types. The same applies to tuples. Tuple types are a combination of their elements' labels and types. Taking labels out of consideration would move tuples closer to being bundled variables, and not distinct types.

1 Like

I think that tuples should remain types (and be more consistently so), and for the same reason I think that it is unfortunate that the following works, and will continue to work even if eg tuple shuffling is deprecated:

func test() {
    let a = (x: 1, y: 2)
    let b = (y: 1, x: 2)
    print(a == b) // true (!)
    print(type(of: a) == type(of: b)) // false
    print(a.x == b.x && a.y == b.y) // false
}
test()
1 Like

It's a little inconsistent, but one could view it as automatic synthesis of == for the two different tuple types. Again, illustrating with structs, because it's easier.

extension S {
   func ==(lhs: S, rhs: T) { return lhs.x == rhs.x && lhs.y == rhs.y }
}

extension T {
   func ==(lhs: T, rhs: S) { return lhs.x == rhs.x && lhs.y == rhs.y }
}

It's inconsistent, in that we don't do it for any types except tuples. (And I am not advocating we start! :upside_down_face:)

In Swift, labels for tuple elements are part of the type.

As a convenience, Swift allows you implicitly to erase labels--that is, let a: (Int, Int) = (x: 1, y: 2)--and to add labels--that is, let a: (x: Int, y: Int) = (1, 2). However, Swift does not allow you to assign a tuple with one set of labels to a variable of tuple type with a different set of labels. This is a deliberate design choice.

None of this has to do with tuple shuffling.

It sounds like you'd prefer a language where labels for tuple elements are not part of the type. That's not the case with Swift.

1 Like

It has, in that tuple shuffling allows what Swift otherwise does not allow, ie to "assign a tuple with one set of labels to a variable of tuple type with a different set of labels":

func test() {
    let a = (x: 1, y: 2)
    var b = (y: 3, x: 4)
    print(type(of: a) == type(of: b)) // false
    b = a // <-- Assigment between tuples of different types.
    print(b) // (y: 2, x: 1)
}
test()

EDIT: I guess you're right because you used "set" rather than "sequence/list" ...

Tuple shuffling is about reordering labels without changing the overall set of labels. Adding or stripping the labels is a separate feature. Anything that messes with the labels that isn't one of those two operations is probably a bug caused by a bad interaction between the two features.

As far as the as? issue goes, the fix-it is wrong—please file a bug!—but the correct thing to write here is as (Int, Int)?. That is, you want to do a compile-time conversion to an Optional tuple value, as opposed to a run-time-checked conversion to a non-Optional value.

1 Like

Btw, here is a way to get the behavior you ask for in the OP (for any tuple) at the cost of one extra character:

// Make a prefix operator that converts any labeled tuple to an unlabeled one:
prefix operator •
prefix func •<A, B>(rhs: (A, B)) -> (A, B) { return rhs }
prefix func •<A, B, C>(rhs: (A, B, C)) -> (A, B, C) { return rhs }
prefix func •<A, B, C, D>(rhs: (A, B, C, D)) -> (A, B, C, D) { return rhs }
// ... add more if needed ...

// Now you can do this:
func test() {
    let counter = 119
    let final: (min: Int, sec: Int) = •counter.quotientAndRemainder(dividingBy: 60)
    print(final) // (min: 1, sec: 59)
}
test()
1 Like