Tuples conform to Equatable

Tuples Conform to Equatable

Introduction

Introduce Equatable conformance for all tuples whose elements are themselves Equatable.

Swift-evolution thread: Tuples Conform to Equatable

This idea has been heavily discussed numerous times before with dozens of threads discussing this issue.

Motivation

Tuples in Swift currently lack the ability to conform to protocols. This has led many users to stop using tuples altogether in favor of structures that they can them conform protocols to. The shift from tuples to structures have made tuples almost feel like a second class type in the language because of them not being able to do simple operations that should just work.

Consider the following snippet of code that naively tries to use tuples for simple operations, but instead is faced with an ugly error.

let points = [(x: 128, y: 316), (x: 0, y: 0), (x: 100, y: 42)]
let origin = (x: 0, y: 0)

// error: argument type '(x: Int, y: Int)' does not conform to expected type 'Equatable'
// or with newer diagnostics
// error: type '(x: Int, y: Int)' cannot conform to 'Equatable';
//        only struct/enum/class types can conform to protocols
if points.contains(origin) {
  // do some serious calculations here
}

This also creates friction when one needs to conditionally conform to a type, or if a type is just trying to get free conformance synthesis for protocols like Equatable or Hashable.

struct Restaurant {
  let name: String
  let location: (latitude: Int, longitude: Int)
}

// error: type 'Restaurant' does not conform to protocol 'Equatable'
extension Restaurant: Equatable {}

These are simple and innocent examples of trying to use tuples in one's code, but currently the language lacks the means to get these examples working and prevents the user from writing this code.

After all the errors, one decides to give in and create a structure to mimic the tuple layout. From a code size perspective, creating structures to mimic each unique tuple need adds a somewhat significant amount of size to one's binary.

Proposed solution

Introduce Equatable conformance for all tuples whose elements are Equatable themselves. While this isn't a general purpose conform any tuple to any protocol, Equatable is a crucial protocol to conform to because it allows for all of the snippets above in Motivation to compile and run as expected.

The rule is simple: if all of the tuple elements are themselves Equatable then the overall tuple itself conforms to Equatable.

// Ok, Int is Equatable thus the tuples are Equatable
(1, 2, 3) == (1, 2, 3) // true

struct EmptyStruct {}

// error: type '(EmptyStruct, Int, Int' does not conform to protocol 'Equatable'
// note: value of type 'EmptyStruct' does not conform to protocol 'Equatable',
//       preventing conformance of '(EmptyStruct, Int, Int)' to 'Equatable'
(EmptyStruct(), 1, 2) == (EmptyStruct(), 1, 2)

It's also important to note that this conformance does not take into account the tuple labels in consideration for equality. If both tuples have the same types, then they can be compared for equality.

// We don't take into account the labels for equality.
(x: 0, y: 0) == (0, 0) // true

Source compatibility

These are completely new conformances to tuples, thus source compatibilty is unaffected as they were previously not Equatable.

Effect on ABI stability

The conformance to Equatable is additive to the ABI and requires a newer runtime to support. Thus one will need a platform, who has declared ABI stability, which embeds a Swift X (replace version here with version this feature shipped with) runtime.

Alternatives considered

Besides not doing this entirely, the only alternative here is whether or not we should hold off on this before we get proper protocol conformances for tuples which allow them to conform to any protocol. Doing this now requires a lot of builtin machinery in the compiler which some may refer to as technical debt. While I agree with this statement, I don't believe we should be holding off on features like this that many are naturally reaching for until bigger and more complex proposals that allow this feature to natively exist in Swift. I also believe it is none of the user's concern for what technical debt is added to the compiler that allows them to write the Swift code that they feel comfortable writing. In any case, the technical debt to be had here should only be the changes to the runtime which allow this feature to work.

Future Directions

With this change, other conformances such as Hashable and Codable would make for other great conformances for tuples. It also makes sense to implement other conformances for other structural types in the language such as metatypes, existentials, etc.

In the future when we have proper tuple extensions along with variadic generics and such, implementing Equatable for tuples will be trivial and I imagine the standard library will come with a conformance for tuples. When that happens all future usage of that conformance will use the standard library's implementation, but older clients that have been compiled with this implementation will continue using it as normal.

42 Likes

Within nominal types, we could have the default definitions of Equatable / Hashable / Encodable / Decodable (recursively) pierce stored properties of tuple type (including enum payloads).

1 Like

This is wonderful progress! I am so glad to see something take shape in this area.

Two brief points:

We have the manually implemented == operators in SE-0015. Would be worth thinking about (a) how they interact with the automatically generated conformance; and (b) whether they could be deprecated in some ABI-stable way.

The alternatives you mention get at a question of degree; that is, how far should we go in terms of creating implicit, magical conformances? I think precedent gives us a good answer in this respect:

  • SE-0015 has more or less settled that tuples of Equatable values should be Equatable and tuples of Comparable values should be Comparable
  • Meanwhile, pre-dating the Swift Evolution process, magical conformance of enums without associated values to Equatable and Hashable gives us a precedent as well
  • By contrast, as a community, we have decided that Codable, Identifiable, and other fundamental protocols should require opt-in

I think this is a workable line to draw going forward. (We could, for example, make Codable synthesize conformance when a nominal type's members include a tuple member with Codable elements without necessarily requiring the tuple itself magically to conform.) In this scheme, then, opt-in synthesizable conformances would have to wait for proper tuple extensions but a very selective group of more fundamental conformances can have their magic extended to tuples.

4 Likes

Now that tuples are Equatable, using == will now always prefer the Equatable implementation. In a sense, now that newer binaries won't be compiled against this == function, it will eventually deprecate itself (we still have to support so that older clients can call it however.)

This is a very important question, and I agree with your evaluation behind which magical conformances we should provide. I've only suggested Codable because it's been mentioned a number of times before, but since there's no current precedent perhaps we should hold off.

Aside that's relevant to conversation: It's also important to state that we shouldn't wait for all of these magical conformances before working on proper implementations. This proposal does, however, allow newer conformances to be added fairly easily, but each conformance should be carefully considered.

3 Likes

I am also very happy to see progress in this area! Thank you for tackling it Alejandro.

I also agree with blessing Equatable, Hashable and Comparable.

6 Likes

Sounds like a very sensible, well-scoped, and useful change!

Could you elaborate on this distinction? I'm pretty sure this doesn't mean that a user could write an Equatable conformance for a tuple type (or you would have said that in the pitch). I also assume there's not going to be a semantic difference: any two tuples that are (un)equal now will still be (un)equal. Just for deeper understanding, what effect does this have, practical or otherwise?

Correct, nobody can currently write an Equatable conformance for some tuple type, and yes, there is no semantic difference. If this is accepted, all clients who currently use == for tuples that compile against this new compiler + runtime will reference the Equatable implementation vs. using the stdlib == for tuples. That's it, just some machinery in the background working.

I think this is the only reason this change might even need a review. The functionality is obvious - it even exists in the standard library today, but in a non-scalable form, making any arguments against the functionality just nonsense.

Given that "it is none of the user's concern for what technical debt is added to the compiler", the question of whether or not to accept your implementation is really down to the owners of the official compiler (i.e. the core team, directly). It's not really a question for the community.

That said, it's really great that you have an implementation! Let's hope the core team decide that it's worth the technical debt :+1:

1 Like

Question—if this is accepted, and a resilient library makes use of magically generated tuple equality, will it be an ABI-breaking change in a future version of Swift to implement a custom conformance when it becomes possible to do so?

2 Likes

Great progress. :slight_smile: One thing that makes me sad is that this requires new runtime support, so even if accepted I wouldn't be able to use it for a long long time. :slightly_frowning_face:

I think this is a necessary change that would solve a lot of issues. Long-term, arbitrary protocol conformance of structural types would be preferable, but I don't see that coming any time soon, so this will be a good interim solution.

2 Likes

Getting Equatable conformance on Void is a huuuuge win for me! Thanks for working on this <3

2 Likes

But the proposal does not state that it will support Void. Either it will in the future draft or it can't because right now it only speaks about tuples with elements, while Void is an element-less tuple.

I apologize for not explicitly stating this in the proposal, but yes it supports Void :slight_smile:

Fun fact: I originally just did this for Void, but some had suggested that just going the extra mile to do all tuples might be better.

9 Likes

The proposal states:

This statement is trivially true for the tuple containing zero elements, so it should receive Equatable conformance. This would be consistent with what we implemented for Equatable synthesis of structs—a struct with no stored properties is trivially equal to itself (because it is the only instance of itself).

It might be good for the proposal to clarify this point, though.

Thanks!

8 Likes

Can someone explain why not taking the labels into account is desirable?

It seems odd to me that these two would be considered equal...

(x: 20, y: 50) == (width: 20, height: 50)

... especially given that, semantically, they represent two distinct ideas (position and size).

8 Likes

I believe doing so would require a breaking change to the tuple ABI. It's also inconsistent with the rest of the language where tuple labels aren't considered part of the type. Edit: The situation w/ tuple labels in the type system is a little more complicated than I remembered

1 Like

It actually goes against current language behavior (where this won't compile):

func foo() -> (x: Int, y: Int) {
    (width: 20, height: 50) <-- Cannot convert return expression of type '(width: Int, height: Int)' to return type '(x: Int, y: Int)'
}

The labels here are taken into account.

4 Likes

It mimics what’s currently implemented with the == operator.

// Now in Swift
(x: 0, y: 0) == (0, 0) // true

Also tuples are weird because (x: Int, y: Int) is both equal to and not equal to (Int, Int)

As unsatisfactory as it is, that's fair enough. It still feels wrong, though, sadly.

Would it be worth the exercise now to consider whether there is enough weight to break with current behavior and to take the labels into account? I realize that's a huge undertaking and likely not possible.

3 Likes