Tuple type inconsistency

It seems to me that there are a lot of edge cases in the tuple system, and they're short-sightedly and inconsistently addressed on case-by-cases bases. I think we need a tuple manifesto, with a well thought out and complete picture of what we want out of tuples.

8 Likes

It's also OK if they are reordered:

let u = (x: 1, y: 2)
var v = (y: 3, x: 4)
v = u
print(u) // (x: 1, y: 2)
print(v) // (y: 2, x: 1)

And same-named labels are OK too:

let o = (a: 1, a: 2) // Same label repeated is accepted.
var p = (   3, a: 4) // The first label is omitted.
p = o // This is OK, but which of o's two a-elements will go into p's a?
print(o) // (a: 1, a: 2)
print(p) // (   2, a: 1) // Hmm, OK ...

When tuples are used as parameters, the rules seem to be a slightly different:

func f(_ tuple: (x: Int, y: Int)) { print(tuple) }
func g(_ tuple: (a: Int, b: Int)) { print(tuple) }

// As expected, the following works:
f((1, 2))       // (x: 1, y: 2)
f((x: 1, y: 2)) // (x: 1, y: 2)
g((1, 2))       // (a: 1, b: 2)
g((a: 1, b: 2)) // (a: 1, b: 2)

// We can call them with the tuple elements reordered:
f((y: 2, x: 1)) // (x: 1, y: 2)

// We cannot do eg:
// f((a: 1, b: 2)) // ERROR: Cannot convert value of type '(a: Int, b: Int)' to expected argument type '(x: Int, y: Int)'

// The labels are shown as part of the function types
print(type(of: f)) // (x: Int, y: Int) -> ()
print(type(of: g)) // (a: Int, b: Int) -> ()
// (But note that the extra parens for the parameter list is missing (SR-8235).)

// They are different types:
print(type(of: f) != type(of: g)) // true

// OK

// BUT: In the following code we are using the same variable h
// to hold first f and then g, even though f and g are of
// different types:
var h = f
print(type(of: h)) // (x: Int, y: Int) -> ()
h = g
print(type(of: h)) // (x: Int, y: Int) -> ()
h((x: 1, y: 1)) // (a: 1, b: 1)

That is, as the OP noted, we cannot convert a tuple to one with different labels:

let t: (a: Int, b: Int) = (x: 1, y: 2) // ERROR: Cannot convert value of type '(x: Int, y: Int)' to specified type '(a: Int, b: Int)'

But we can cast a tuple type to one with different labels if it's in a parameter:

let i: ((a: Int, b: Int)) -> () = { (tuple: (x: Int, y: Int)) in print(tuple) }

And a somewhat related example:

func j(_ tuple: (x: Int, y: Int)) { print(tuple) }
func j(_ tuple: (a: Int, b: Int)) { print(tuple) }
// It's OK to overload j, because the two funcs have different types.
// They can only be called with correct labels (reordered or partly omitted ok)
// as expected:
j((x: 1, y: 2)) // (x: 1, y: 2)
j((a: 1, b: 2)) // (a: 1, b: 2)
j((b: 1, 2))    // (a: 2, b: 1)
// And without labels it is an ambiguous use of j as expected:
// j((1, 2)) // ERROR: Ambiguous use of `j`
// But for some reason you cannot disambiguate it by giving the type explicitly:
// let k: ((x: Int, y: Int)) -> () = j // ERROR: Ambiguous use of `j`
// let k = j as ((x: Int, y: Int)) -> () // ERROR: Ambiguous use of `j`

I agree that we need a manifesto, and as mentioned elsewhere, I also think we need a clear documentation of the currently intended design and behavior of tuple types (if there is one?), because the way things are now, it's not straight forward to tell intended behavior from bugs.

3 Likes

Dear @Jens, thank you for all the work and labour you put into the tuple issue.

Currently, I like to think of tuples as an intermediate shortcut… but of course that's not really true… Aren't tuples also used to import c-arrays or something like that…?

Anyway. Yes. We have to sort things out. We have to make tuples consistent (unlabelded tuples of one?), and properly place them in the (type) system. And there is plenty of room to see a bright future for them, probably guided by a well advised manifesto… ;)

I say this as a simple user and bug-reporter, but afaics, they are still intertwined with lots of different parts of the language implementation, function types, enums with associated values, pattern matching, etc.

In general, == can be implemented for comparison of two values of heterogeneous types. It is not restricted only to implementations of the Equatable requirement. Just because you can evaluate equality of two values using == does not mean that they are of the same type.

There are many issues and bugs with the implementation of tuples, but this is not one of them.

3 Likes

I think I will report this as a bug, as this violates the basic notion of equality.

For example:

var p1 = (x: 1, y: 1)
var p2 = (a: 1, b: 1)
p1 == p2

func f(_ tuple: (x: Int, y: Int)) { print(tuple) }
f(p1)
f(p2) // Not allowed

If p1 and p2 are considered equal values, I should be able to use them interchangeably.

This isn't a bug. p2 is of type (a: Int, b: Int), which doesn't match the expected argument type. Type equality and value equality are not the same. As I mentioned before, tuple types without labels can be implicitly converted to their labeled analogues, or vice-versa explicitly. For instance,

var p1 = (x: 1, y: 1)
var p2 = (1, 1)

func f(_ tuple: (x: Int, y: Int)) { print(tuple) }
f(p1)
f(p2) // Allowed

But differently labeled tuple types are not interconvertible. Tuple values can be checked for equality if their types match without labels (if their values are of the same types). That said, there indeed are quite a few of inconsistency moments, but certainly not this one.

Then how about this:

var a = (x: 1, y: 2)
var b = (y: 2, x: 1)

// All their elements are equal:
print(a.x == b.x) // true
print(a.y == b.y) // true

// But the tuples themselves are not equal:
print(a == b) // false

// We can assign them to each other:
a = b // OK
b = a // OK

// But even now, after `b = a`, they are still not equal:
print(a == b) // false

// But watch this:
a = (x: 1, y: 2)
b = (x: 2, y: 1)
print(a == b) // true(!)

// And this:
a = b
print(a == b) // false(!)
4 Likes

Now this is already worth filing. The following shows how your example is a bug, and the fix should be certain about the order sensitivity, because that's probably where it trips.

var a = (x: 1, y: 2)
var b = (yy: 2, x: 1)

a = b // Cannot assign value of type '(yy: Int, x: Int)' to type '(x: Int, y: Int)'

@Jens The yy is intentional.

I'm not sure what you mean, could you please clarify? Do you think tuple shuffling/restructuring should be prevented?

Either way, it's messed up. If 'equal', but differently ordered tuple types ((x: Int, y: Int), (y: Int, x: Int)) were implicitly interconvertible, (x: 1, y: 2) should be equal to (y: 2, x: 1) by value. If they aren't, they shouldn't be assignable to each other by type.

I don't think shuffling is a problem, it makes sense. I don't know the intended behavior though. The bug should clarify that.

Exactly, that's why I think we need a manifesto and documentation of the currently intended behavior (assuming there is one).

Without a common understanding of the intended behavior (among both compiler devs and users), reporting and "fixing" tuple related bugs seems like a game of chance and might be counterproductive.

1 Like

It’s no more incorrect than this:

class Two: Equatable {
  var zero, one: Int
}
class XY: Two {}
class AB: Two {}

var p1 = XY(zero: 1, one: 1)
var p2 = AB(zero: 1, one: 1)

// p1 = p2 // doesn’t compile
p1 == p2 // true

In the second example, p2 can’t be assigned to p1 because the variables have different classes, but they can both be converted to a common superclass, Two, and when they are they’re equal.

Similarly, in the first example, p2 can’t be assigned to p1 because the variables have different types, but they can both be converted to a common supertype, (Int, Int), and when they are they’re equal.

That may help you understand some of these seemingly odd behaviors from Swift.

2 Likes

I agree, but I can't exclude the possibility of this being a trivial case (the intended behavior of which is clear to the core team)

1 Like

Perhaps @beccadax can say whether and why my above example demonstrates any bug(s).

Your example demonstrates a feature known as tuple shuffling. @codafi has sketched out a proposal to remove that feature, and the thread(s) about that go into great detail about its history and whether it ought to be a part of the modern tuple type. Yes, it is one feature that is carried over from an older model of tuples.

However, the OP has a question that doesn’t involve tuple shuffling. @beccadax and @anthonylatsis have both explained why the original example behaves that way. There is no need to muddy the explanation here with discussions about unrelated features and bugs. No bugs are involved here with respect to the OP’s question.

The example of the OP:

var p1 = (x: 1, y: 1)
var p2 = (a: 1, b: 1)
// p1 = p2 // Doesn't compile
p1 == p2 // true

My example (essentially):

var p1 = (x: 1, y: 2)
var p2 = (y: 2, x: 1)
p1 = p2 // Does compile
p2 = p1 // Does compile
print(p1 == p2) // false, even though p2 = p1

I think my example is clearly related to the one of the OP, and interesting to mention in this context.

The OP was unsure about wether his example was intended behavior or a bug.

I was unsure about wether my example was intended behavior or a bug.

The real solution is of course to clearly document the intended behavior.

IMHO pretending or advocating that each tuple related question/issue should only be handled on a case by case basis, isolated even from clearly related examples, is to muddy the explanation.


Thank you for mentioning that, I wasn't aware of it.

Again, your example involves a feature called tuple shuffling. It is not unintended behavior, but it is undocumented, and it is an active topic of discussion with regards to whether it should exist in the future.

OP's example does not involve that feature. It is not unintended behavior, it is documented (and has even gone through Evolution), and it is not an active topic of discussion except here as a question about usage.

This isn't the Evolution forum, and there's no need to turn every topic about tuples into a debate about how it should be designed into the future.

1 Like

Without being aware of the undocumented feature of tuple shuffling (let alone it's possible deprecation), my example certainly looks like it is clearly related to that of the OP, so my mentioning of it and your explanation helped the OP, me and other readers of this thread to get a more complete answer to the original question.

I think every Swift user and compiler dev should be concerned about the currently (not future) intended behavior not being documented.

As seen above (and in almost every tuple related thread), it's not only me who is confused.

After years of bumping into and reporting what often turns out to be tuple related bugs, no indication of things getting better / less confused, and no clear acknowledgement from the core team that the currently intended design need to be clearly documented, I've made it a habit of pointing this out.

That said, I will stop if you and a couple of other people more experienced than me tell me to. But I will also not report any more bugs unless I'm able to refer to said documentation.

Look, I'm glad you're so passionate about the topic. There's a time and place for discussing problems, which are very much real, and I for one hope you stay passionate about that.

In this specific instance, a complete and correct answer to the OP's question exists based on existing, documented, intentional, non-buggy behavior. We don't need a manifesto about the history and direction of language and compiler design, nor does the OP need to bash against every sharp edge that remains in the language and walk away with the erroneous impression that nothing is documented and everything is up for grabs about tuples.