Tuples conform to Equatable

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

Tuples already support lexicographic comparison. This pitch just makes that available as a Comparable conformance.

That would need to be a standalone pitch. The issue here is that the synthesized Comparable conformance would depend on source order and would change if the source is edited and the properties change order. My guess is there would be a heated debate over whether this is acceptable or not.

4 Likes

Unrelated, but I hate that those conversions are allowed. They can cause surprising performance cliffs.

It is a fairly significant limitation of the language today that structural types flat out do not support protocol conformance today, and it is a language change to break that assumption. I wouldn't personally expect much contention from the high-order idea that tuples have these protocol conformances, but adding them as a special case does have interactions with future language directions (allowing structural types to conform to protocols, variadics, and such) that need to be considered, and for some conformances like Codable, there are design decisions to be made about what the behavior should be.

5 Likes

I think there probably ought to be a proposal and review for this change, but to play devil's advocate, SE-0015 arguably already proposed this change and was accepted:

The Swift standard library should provide generic implementations of the comparison operators for all tuples up to some specific arity. The arity should be chosen so as to balance convenience (all tuples support this) and code size (every definition adds to the size of the standard library).

When Swift gains support for conditional conformation to protocols, and if Swift ever gains support for extending tuples, then the tuples up to the chosen arity should also be conditionally declared as conforming to Equatable and Comparable .

If Swift ever gains support for variadic type parameters, then we should investigate redefining the operators (and protocol conformance) in terms of variadic types, assuming there's no serious codesize issues.

The "some specific arity" just happens to be infinite. :^)


Yes—lexicographic comparison from left to right. SE-0015 already added overloads of < and friends to implement precisely this behavior for tuples of 1-6 elements; they just aren't formally modeled as a Comparable conformance.

This was discussed in SE-0185:

The original discussion thread also included Comparable as a candidate for automatic generation. Unlike equatability and hashability, however, comparability requires an ordering among the members being compared. Automatically using the definition order here might be too surprising for users, but worse, it also means that reordering properties in the source code changes the code's behavior at runtime.

This logic doesn't apply to tuples because reordering tuple elements can, and often does, break use sites; reordering properties usually doesn't.

This is a a discussion we could reopen, of course, but it's a pretty distinct discussion from tuple conformances; I don't think we should try to have both simultaneously.

8 Likes

Actually you can do much better than that. Let's first view a protocol as a type, and pretend we have subtyping. Then there's a standard rule, that product types are covariant, variant types (enums in Swift) are covariant, and function types are contravariant in their domain and covariant in their codomain.

Its unfortunate swift pretends functions can have multiple arguments instead of just relying on tuples, but lets ignore that.

So in summary, subtyping is actually quite easy to support for structural types such as tuples. Its actually nominal subtypes that are hard (because they require table lookups to decide if X is a subtype of Y).

So now what we can say is that if we have a subtyping relation, including considering that a type T conforming to a protocol P might hold, then that relation also holds for tuples on a component by component basis.

The real question is: why waste time on a proposal that ONLY extends tuples with a small fixed set of such relations such as Equatable, Hashable, etc, instead of all of them. The implementation in the unification engine is trivial.

Indeed given extensible variants (enums), they too support covariant subtyping on both a width and depth basis. Actually tuples support width subtyping too but it is likely to be very confusing losing the tails of tuples accidentally.

Also: MutableCollection & RandomAccessCollection for homogeneous tuples? Such as those imported from C fixed-size arrays.

  • An unkeyed container for a tuple with fully unlabelled elements?
    e.g. (123, 456) encoded in JSON as [123, 456]

  • A keyed container for a tuple with fully labelled elements?
    e.g. (x: 123, y: 456) encoded in JSON as {"x": 123, "y": 456}

  • Which container for a tuple with partially labelled elements?
    e.g. (123, z: 456) encoded in JSON as [123, 456] or {"0": 123, "z": 456}

I think that would also be interesting to consider. It too would probably deserve a design discussion and proposal of its own.

2 Likes

Swift used to pretend functions take a single argument (which might be a tuple) and this caused no end of trouble. It's a nice model in some languages, but not a good fit for Swift, for various reasons:

  • default arguments
  • variadic arguments
  • inout arguments
  • generic substitution (eg, Array<T>.append(_: T) with T a tuple type)

The new model was a conscious change that required a fair amount of work to plumb through but the result was a simpler and more internally consistent language model and implementation.

12 Likes

Interesting. Felix has single argument functions. It also has default arguments, variadic arguments, and supports a way to do input/output (differently). Functions cannot have multiple arguments, so what you actually have is that each function is a combination of a function that takes a single argument underneath, and an N-functor to construct it. This creates no end of problems generalising higher operations. With tuples the higher operations are isolated to the sole N-functor which creates a tuple. C++ has the same problem. So does Rust.

For example, function composition is trivial if you only allow one argument. It's impossible if you have two arguments, because no function can return two arguments, only a tuple. So you actually have to map a function of two arguments to one which has only a tuple first, before doing the composition, and you need a different map for every number of arguments to be general.

In any case its too late to fix. I agree, tuples should support equality. And they should also support quite a lot of other protocols. My primary concern with the proposal is that it doesn't go far enough. It would be better if the language could be extended, so that the specialisation of a protocol to tuples could be written in the language, rather than a few select ones being implemented in the compiler.

In Felix, user code provides equality, total ordering, and conversion to string for all tuples. The way this is done is to provide a second model of tuples similar to that used for lists: a head and tail representation. This allows polymorphic recursion to process a tuple of arbitrary length. In Felix the result type must be invariant. I think you need GADTS otherwise, which Felix has, but are hard to work with. The main problem is .. well .. it won't work for functions that take a list of arguments. So another feature needs to be added, to normalise function representation using a list of arguments back to using a tuple, and then one to unpack the tuple back to a list.

Hash is different. A tuple hash isn't a combination of component hashes. For hashing, any object represented in a contiguous sequence of bytes can be hashed by hashing the byte sequence. So a tuple of value types can be hashed, even if the component value types can't. So this problem could be solved by simply saying all value types are hashable. Stuff gets hard when you have pointers in there.