Tuples conform to Equatable, Comparable, and Hashable

If I understand this correctly, we would still have the following unfortunate situation, and worse:

var a = (x: 12, y: 34)
let b = (q: 12, r: 34)
print(a == b) // true
a = b // ERROR: Cannot assign value of type '(q: Int, r: Int)'
      //        to type '(x: Int, y: Int)'

That is, not only will values of different types continue to compare equal using the equality operator, they will also be equal according to their Equatable protocol conformance.

How can two values be Equatable, Hashable and Comparable when they don't even share the same type (and cannot be assigned to each other or stored in the same Set etc)?

Will this introduce the first and only case in Swift where two values of different types can be equal according to their Equatable conformance?


To make this consistent, some bigger change seems necessary, like deciding that labels are irrelevant for a tuple's type, ie that only the elements decide the type. But I guess this would be a source breaking change, since in current Swift, we have this craziness:

var c = (x: 12, y: 34)
let d = (y: 12, x: 34)
print(c == d) // true
c = d
print(c == d) // false

I'd rather see the whole tuple mess cleaned up before building more stuff on top of it.

13 Likes

Labelled tuples can be assigned to variable of type unlabelled tuple, so they can be though of as a subtype of unlabelled tuple. (and also a supertype, but that's not relevant) If you think of them that way, then it happens with other types already:

class Super: Equatable {
    static func == (lhs: Super, rhs: Super) -> Bool {
        true
    }
}
class A: Super { }
class B: Super { }
print(A() == B()) // true
3 Likes

True, thanks!

I'm still wondering about this though:

var c = (x: 12, y: 34)
let d = (y: 12, x: 34)
print(c == d) // true
c = d
print(c == d) // false

Note that if we explicitly "downcast" d to (Int, Int) prior to assignment, it will work as (perhaps most commonly) expected:

var c = (x: 12, y: 34)
let d = (y: 12, x: 34)
print(c == d) // true
c = d as (Int, Int)
print(c == d) // true
2 Likes

Yeah. Tuple shuffles seem like a mistake. They cause weird problems, and are not that useful.

5 Likes

If we added Collection for homogenous tuples, and some kind of syntax shorthand to declare them, wouldn’t it essentially give us fixed-size arrays?

Is it worth going for that now, while we can, or waiting for something like generic value parameters? Bearing in mind that we already import fixed-size arrays from C as tuples, so any FSA design that isn’t a tuple will have large source breakage.

(I'd like to know that too. And, as you already know, but as a convenience for others, I've an unanswered question about essentially that here.)

Thank you for working on this @Alejandro! I’ve wanted it for a long time and am very eager to see it happen!

I don’t think this analogy is a good one. Typically the result of comparing instances of different subclasses would be false, not true.

I don’t really have a problem with an unlabeled tuple being comparable to a labeled tuple when the types match up. But I think label shuffling is bad especially since the members are not compared labelwise. It’s even worse if two tuples with entirely different labels are comparable just because the types happen to line up.

If this is the behavior we already have with the comparison operators in concrete code this proposal certainly shouldn’t be expected to change that. But I do have some questions about how labels will be handled in generic code.

func compareValues<T: Equatable>(_ lhs: T, _rhs: T) { ... }

// Is this allowed?
compareValues((1, 2), (x: 1, 2: y))

// What about this?
compareValues((y: 1, x: 2), (x: 1, 2: y))

// What about this?
compareValues((a: 1, b: 2), (x: 1, 2: y))
4 Likes

Random question, shouldn‘t tuple also conform to Error when all types conform to Error as some kind of accumulator of errors which itself would be considered as an error?

I wouldn’t make such a sweeping generalization. Consider class clusters where there’s one public superclass, and a number of subclasses (which may or may not be public) providing specialized implementations.

It is entirely reasonable to say that two instances of the superclass (which might actually be instances of subclasses) should compare equal or unequal based solely on their salient data, not their implementation details.

1 Like

Yes, class clusters are an exception to that statement. Thus use of the term "typically" to soften the statement precisely so it isn't a sweeping generalization.

In any case, class clusters are certainly irrelevant to the present discussion. (x: 10, y: 10) and (a: 2, b: 2) do not have a relationship remotely like the kind seen in a class cluster. In a class inheritance analogy, they are far more similar to independent subclasses that make up part of a user-visible API surface.

2 Likes

Counterpoint: yes they do.

The supertype is (Int, Int), and the two subtypes do not add any stored properties, just some convenience accessors.

• • •

Regardless, as I mentioned here, I would prefer to make tuple labels have no type-system significance at all. If someone wants labels to affect type, then they should be using structs, not tuples. The labels of a tuple should exist purely for convenience.

2 Likes

You're only talking about the subtype / supertype relationship. This is present in all class hierarchies. Class clusters are a very particular kind of class hierarchy. Tuple labels are not just "convenience accessors". They indicate the semantics of the data contained by the tuple. Different labels imply different semantics.

I'm not sure it matters what anyone thinks about this topic now. As far as I know, changing this behavior is not on the table as it would be a significant breaking change.

This would provide a ton of utility. The lack of this kind of feature is one of the main reasons I don't use Tuples very often so I would love it to be available in the language!

However, my opinion on this is completely dependent on the expected timeline for generalized Tuple protocol conformance. If we're talking sometime in the next year or two, I think it makes sense to wait. Otherwise if it's more of an "eventually it will probably be added", the use case for this pitch seems valuable enough to warrant being added soon.

1 Like

The existence and behavior of tuple shuffles is, in my estimation, orthogonal to whether tuples conform to protocols. We already have == and < operators that take tuples, and work with labeled tuples via the existing implicit conversions, and it doesn't seem to me that making them formally Equatable and Comparable worsens the situation. And if we change the language later to reduce or eliminate these implicit conversions, then we'll get the benefits whether or not tuples have protocol conformances.

12 Likes

I tend to agree with this assessment. I don't see anything in this proposal which would introduce obstacles that don't already exist when it comes to evolving tuple conversions in the future.


Overall, I think this is a very well-scoped proposal. The examples from the motivation section are particularly compelling because they show how these three conformances will enable usage beyond the immediate protocol requirements (contains, dictionary keys, etc.). I'm less sure we should introduce conformances to protocols like Codable in this manner, but Equatable, Hashable, and Comparable seem like clear wins.

1 Like

This seems to me to be a clear case of perfect being the enemy of good. Even if the current situation with tuples is not what we'd like (and I'd suggest not using terms like "mess" and "crazy" for what seems like some fairly minor inconsistencies/inconveniences), holding back such a useful addition as this would need to be justified by examples of real harm of the interaction between the new feature and the existing deficiencies.

6 Likes

And to be clear, if (a: 1, b: 2) == (b: 2, a: 1) compiles, that's because the compiler is inserting an implicit conversion into the argument expressions. == would still be invoked on a single specific Equatable type in that example.

1 Like

This would also be the case in generic code like I posted above, correct? So it would just see (Int, Int) and the implicit conversion happens outside the generic code from the labeled to the unlabeled tuple types? If that's the case then I agree with your assessment that they're orthogonal issues.

Fwiw, I think the ideal would be to allow promotion to unlabeled tuples, but not promotion to the same unlabeled tuple type from differently labeled tuples in the same expression, as in the generic code example I posted above. i.e. the first example would work and the second two would not.

1 Like

That's right.

(swift) func equals<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
          lhs == rhs
        }
(swift) equals((1, 2), (x: 1, y: 2))
// r0 : Bool = true
(swift) equals((y: 1, x: 2), (x: 1, y: 2))
<REPL Input>:1:1: error: type of expression is ambiguous without more context
equals((y: 1, x: 2), (x: 1, y: 2))
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(swift) equals((a: 1, b: 2), (x: 1, y: 2))
<REPL Input>:1:1: error: conflicting arguments to generic parameter 'T' ('(a: Int, b: Int)' vs. '(x: Int, y: Int)')
equals((a: 1, b: 2), (x: 1, y: 2))
^
2 Likes
Terms of Service

Privacy Policy

Cookie Policy