Tuples conform to Equatable

The situation is weird because tuples of the same element types but different labels are currently equal (by ==) but their types are not equal (by type(of:)).

Also, tuples with different labels but the same element types cannot be assigned to one another, unless one of the two has no labels.

Which means transitively, by going through a temporary with no labels, tuples with different labels can be assigned to each other, there’s just an extra step involved.

• • •

Some possibilities:

• The proposed solution ignores tuple labels and just compares elements, which matches the behavior of the existing hard-coded == operators for tuples.

• An alternative could respect tuple labels, meaning if the labels don’t match then the Equatable conformance does not apply (because the types are different), but it would still be possible to use the existing hard-coded == operators for those tuples.

• We could declare that tuple labels always matter (except possibly when one side has no labels), which would involve the source-breaking change of removing the existing == comparison operators for tuples. This is probably a non-starter for source-compatibility reasons.

• We could declare that tuple labels never matter (except for enabling convenience accessors), which would make tuples purely structural. In that world, any time that labels matter would be a job for struct.

The last option is conceptually the simplest, and it is similar to what was done for function types in SE–0111: Remove type system significance of function argument labels.

3 Likes

I read the proposed implementation PR :slight_smile:

1 Like

The current behavior arises because of the static type system conversion rules. (x: Int, y: Int) and (Int, Int) are distinct types, but the compiler automatically converts between them. If you formed, say, Foo<(x: Int, y: Int)>, that would give you a distinct type from Foo<(Int, Int)> without an implicit conversion between them. I would expect Equatable methods to behave similarly; statically, if we can convert two different tuple types to a common tuple type, we would do so, and use that one common tuple type as the Equatable-conforming type to call the method with dynamically.

9 Likes

This is much-needed functionality! One important implementation concern, though, is how to deal with backward deployment to older runtimes where tuples do not have any protocol conformances. There are couple of possibilities I can think of:

  • The tuple-equatable conformance is only available when targeting an OS with Swift 5.2 or later. This wouldn't require any backward deployment hackery, but would likely require us to design and implement the general feature of how conditionally available protocol conformances work in the language.
  • We use the backward compatibility library to backward-deploy this functionality into binaries targeting older OSes. This would allow users to take advantage of the functionality without restrictions*, but would require that we actually have enough hooks in the 5.0 and 5.1 runtimes that can be replaced to understand tuple conformances. In theory, it seems like this is possible, since swift_conformsToProtocol ought to be hookable, and we could provide copies of the runtime data structures for the conformance in the compatibility library, but it would require implementation effort to make sure this is actually possible.

(*And the hooks have limitations of their own—they only take effect when linked into the main binary, not dylibs, so dynamic libraries that want to be distributed for use with older Swift toolchains that don't have the compatibility library would still not be able to take advantage of the new functionality.)

10 Likes

How would (1, 2) compare to (2, 1)?

The usual answer is "elementwise": the first element that differs defines the ordering. In that context, (1, 2) is less than (2, 1).

Indeed, it already works that way; see SE-0015.

2 Likes

And tuples are even more obviously ordered than enums, avoiding the quandaries that deterred Comparable synthesis for structs.

I’m very much in favor of this proposal, assuming we resolve the issues Joe outlines, but think we should introduce all appropriate synthesized conformances in one shot.

13 Likes

Agreed. I hadn't realized (or forgot) that the comparison operators were already defined for tuples.

Lovely. I do hope we get the ability to handle this for user defined protocols soon but I agree that waiting isn't the right choice.

The Core Team today discussed this proposal and how we'd like to manage review of this and related functionality. We agree that the lack of these conformances is a significant pain point in the language and that it's worth considering introducing special case behavior until more general mechanisms for extending structural types are available. As far as structuring the review discussion goes:

  • We think that there is a core set of protocol conformances that clearly make sense for tuples, and have an obvious implementation that the standard library would provide if the language today allowed for it: Equatable, Hashable, and Comparable. The core team would like to see a single proposal covering these conformances.
  • We also think that tuples should clearly conform to Codable. However, there are enough implementation details to discuss about how tuples ought to be coded that the core team would like to see some discussion of this aspect. If there is a lot of contention about these details, tuple Codable conformance may deserve its own proposal and review.
  • Other categories of structural types also have obvious conformances, such as metatypes being Hashable and existentials being Hashable when their protocol constraints imply Hashable, and these deserve separate proposals as viable implementations become available.

In the ensuing discussion, we'd like to see attention paid to the long-term compatibility tradeoff of special-casing these conformances today, vs. what future language directions such as variadic generics may enable. If we accept any of these proposals, we would be committing to preserving compatibility with the source-level behavior, and on ABI-stable platforms, any runtime behavior and ABI requirements this introduces, even when more general mechanisms get introduced in the future. It would be good to explore and enumerate what these long-term liabilities are so that we know what we would be committing to.

24 Likes

Should we mark up tuples that we want to apply Equatable et al to, while keeping the default tuple expressions and types protocol-less?

// Made up syntax
@(1, "3").hashValue

I just came up with that syntax after reading the referenced post and remembering my most recent multiple-dispatch idea. (The "@"-marked tuples are the only ones that we could arbitrarily extend and allow multiple dispatch through. Technically, adding Equatable only to these tuples is also multiple dispatch, I think.)

I can’t imagine any reason why we would want to introduce a new kind of type instead of just giving the existing tuple types conformances. This would be unnecessary complexity, would be confusing for many people, and would introduce undesirable impedance mismatch between code that traffics in the different kinds of tuples.

6 Likes

I proposed a tuple variant because I don't know if just making current tuples suddenly match protocols would break either source- or ABI-compatibility.

This was hidden within my previous reply to this post, but maybe we could do general multiple-dispatch via extensions to tuples, and institute tuple-conformance to Equatable (and the rest) via that.

I know that functions that take an Optional parameter can use a non-optional instance as an argument. Do tuples have the same relationship? I mean that is a tuple with at least one label member a sub-type of a tuple with the same shape but (at least) some labeled members become unlabeled? If so, we would define the tuple conformances to Equatable as an extension on unlabeled tuples, and ones with at least one label would inherit that.

If we do implement Equatable tuples as multiple dispatch, would we have to wait until variadic generics are implemented so we can define the extension just once? Otherwise, we need to extend Void, then all two-member unlabeled tuples, three-member, etc. up to some limit like we do now with the tuple == functions. (The new definitions would still work better because, unlike the current function versions AFAIK, they would work when a member is itself a tuple.)

I'll just repeat what I said before: I don't even think this needs a review. There have been times when the core team just straight-up added obvious conformances without a formal proposal, and I think that also makes sense here.

IIUC, swift-evolution is about the language, not about how the official compiler implements anything, and the evolution process is just about gathering feedback for the core team to make a decision. Is there really any useful feedback to be gained from a review?

I'll also note that I'm generally fairly critical of the core team doing things without enough community involvement, but even I don't think it's necessary in this case. Of course, if the core team really feels they want/need that feedback, that's up to them.

I suppose we wouldn't even see this in an ABI-stable platform until the next round of major Apple OS releases. Obviously, nothing has been announced, but that typically happens around September/October. Variadic generics was mentioned in the Swift 6 roadmap. Is it likely that variadic generics can be implemented by that time? Perhaps we can keep this special-casing as a fallback solution?

Variadic generics wouldn't help metatypes or existentials, though, and AFAIK there are no alternative implementation strategies planned for them, so special-casing appears to be the only option if the core team feels those conformances are worth it.

I mentioned this multiple times in different threads that personally I would find it cool if we had tuples as a struct while the general tuple syntax would become a tuple literal syntax which could also be used for other types such as for the python lib I believe.

Can we once and for all debunk if we could make this happen and keep source compatibility in the future. Obviously we would need variadic generics and at least generic parameter type labels.

struct Tuple<variadic Element>: ExpressibleByTupleLiteral { ... }
extension Tuple: Equatable where Element: Equatable { ... }
extension Tuple: Hashable where Element: Hashable { ... }
extension Tuple: Encoding where Element: Encoding { ... }
extension Tuple: Decoding where Element: Decoding { ... }

This would also allow for single element labeled tuples as we would have an unambiguous syntax (e.g. Tuple<label: Int>) and allow the average swift user to extend the type with custom protocol where appropriate.

7 Likes

Imho adding conformances for tuples is a good idea, but I wish there was a bigger plan what should happen in the long run, instead of allowing some basic protocols in an ad hoc fashion.

  • Will tuple conformance always stay a special-case feature?
  • Will it be limited to protocols that only define operators (and some special cases)?
  • Or are we going to see extension (Int, Int): SomeProtocol in the future?

I can't see any downsides for the last option, as it fits great into my model of tuples as anonymous structs - but I know that for every possible change, you can find someone who opposes ;-)

Thinking about this, I might have found a downside myself ;-): When you allow custom conformances, you can have incompatible implementations - this does not happen when everything is handled by the compiler. But the same problem already exists for other types, so imho that's not a dealbreaker. It's still worse for tuples, though...

4 Likes

Not quite true, depending on what exactly we mean by "different labels". This little program shows what I mean, and more (this is current behavior in Swift 5.1):

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

print(a.x == b.x && a.y == b.y) // true
print(     a     ==     b     ) // false

a = b         // <-- Assigning despite `a` and `b` having different labels.
print(a == b) // false
b = a         // <-- Assigning despite `b` and `a` having different labels.
print(a == b) // false

: O

Those assignment are allowed because of an accepts-invalid bug, right?

Expected behavior: The labels of a should be considered different from the labels of b, because tuple element order matters, and thus the assignment should be an error.

Observed behavior: The labels of a are considered the same as the labels of b, as if tuple element order didn't matter, and thus the compiler accepts invalid code.

( Filed SR-12112 )

4 Likes

Is there really an obvious (and mandatory, non-customizable) Comparable-conformance that would make sense for eg these:

let person = (firstName: "Ada", lastName: "Lovelace", birthDate: Date(/*...*/))
let coordinate = (latitude: 40.689263, longitude: -74.044505)
let dmy = (day: 30, month: 1, year: 2020)
let ymd = (year: 2020, month: 1, day: 30)  

?

If so, I assume that the same Comparable conformance will be automatically synthesized for struct, ie that the following will compile in a future version of Swift:

struct ImperialMeasure : Hashable, Comparable { // Error (currently, because of Comparable)
    var inches: Double
    var feet: Double
    var yards: Double
    var miles: Double
}

I couldn't come up with a less contrived (and stupidly designed) type, but you get the idea: The order of the stored properties would make or break measureA < measureB semantically (something which is not the case for measureA == measureB and hashing).

What I'm trying to say is that while needing to write custom Equatable (and Hashable) conformances is not uncommon (there are cases where the automatically synthesized conformances won't make sense), it would probably be much more common for Comparable, perhaps even so common that it would be better to leave it as it is (ie no automatically synthesized Comparable conformances). And if so, that would probably mean that there is no obvious Comparable conformance that makes enough sense for all tuples to be worth its weight (in potential confusion etc) either.

Sorry if all of this has already been discussed or clarified elsewhere.

1 Like
Terms of Service

Privacy Policy

Cookie Policy