Tuple type inconsistency

I came across the following today:

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

So p1 and p2 are considered to be of different types (due to the different element names), yet they are considered equal.

Is this a bug, a known issue, other?

AFAIK a deficiency of the (labeled) tuple system, more or less intended somehow.
This 'works':

var p1 = (x: 1, y: 2)
var p2 = (a: 2, b: 1)
p1 = p2 as (Int, Int) // works

o_O

Yeah, tuple types with different labels are different. However, types without labels can be implicitly converted to their labeled analogues, or vice-versa explicitly, both cases illustrated in @torquato's example. Comparison is possible when the types match without labels (they get their labels stripped off before being compared), which actually does make sense. As non-nominal value types, they are basically compared with an element-wise value comparison.

1 Like

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.