SE-0283: Tuples conform to Equatable, Comparable, and Hashable

It just occurred to me that the current hashable implementation doesn't hash anything for void, what's the correct hash value for void? Is this just 0 like Optional<T>.none ? @Joe_Groff @lorentey

Iā€™d like to know more about what the community expectation is for certain behaviors.

What do I expect when the values are in different order?

(1, 3) > (3, 1) // does this resolve true or false? Why?

Should a Hashable, Comparable tuple of Int behave like a Set?

(4, 3, 2, 1) == (1, 2, 3, 4) // True??

It feels like similar tuples should ā€œjust workā€? What about touples with different member counts?

(1, 1, 1, 1) > (4, 1) // What happens here? Why?

This proposal puts extra burden on the developer to understand new implicit behaviors. I worry that implicit behaviors are more complex, harder for developers to understand, and therefore could cause unexpected results.

2 Likes

There's still something of a subtle distinction hereā€”if you have a protocol P with a requirement x, even if there's an implementation of P.x in an extension, if a concrete type S: P implements x, then it doesn't matter whether you access it through the concrete type or an existential, you're always going to get S.x.

Is what's happening here that the tuple conforms to Hashable but an element named hashValue is never a made a witness for the Hashable.hashValue requirement, even though they happen to share the same identifier? (Similar, but not the same as, how @_implements lets a member of a different name be a witness of a requirement?)

Assuming I'm not misunderstanding so far, the topic of having non-nominal types like tuples conform to arbitrary protocols has come up many times before, so how would that look compared to the hashValue behavior you describe? If this proposal allows a tuple to conform to Hashable but a hashValue-labeled element doesn't witness the same-named requirement, then would the same behavior have to generalize to all protocols? If so, then imagining we had a syntax to express this conformance:

protocol P { var x: SomeType { get } }
// strawman syntax
extension (x: SomeType, y: SomeOtherType): P {}

We're left with one of three choices:

  1. Tuple's x is a witness for P.x, which differs from the synthesized hashValue behavior described here

  2. Tuple's x is not a witness for P.x, which makes the conformance brittle/a huge footgun, because it will behave differently depending on whether the tuple is passed as P or <T: P>

  3. We make some other decision, like this explicit extension is what makes the tuple elements witnesses of the protocol requirements, and so the user could still do this if they wanted a hashValue witness:

    extension (hashValue: Int, otherFields: BlahBlah): Hashable {}
    

This is thinking quite a bit ahead, and I don't want to use this review as a place to anticipate or design future language features (overall this proposal is great and fills a much-needed gap), but I do want to make sure we don't paint ourselves into a corner later on.

2 Likes

@mcritz, as noted above, these are not new behaviors. What you write above regarding Equatable and Comparable has been available since Swift 2.2 (minus the formal conformance to the protocols); the behavior of these operators was reviewed in SE-0015 over four years ago:

(1, 3) > (3, 1)              // false
(4, 3, 2, 1) == (1, 2, 3, 4) // false

Tuples of different arity are, of course, distinct types; such values cannot be compared to each other using homogeneous comparison operators.

5 Likes

That isn't necessarily true, since S could have members named x besides the one that witnesses the x requirement, and with things like @_implements, the implementation of P.x might not even be named x in the context of S.

That's what I would expect. In the context of a conformance applying to all tuple types, the conformance doesn't know whether the tuple has any particular labeled members at all, so none of the labeled fields are visible in the context of the conformance, and so none of them are eligible to witness requirements for the conformance.

1 Like

I suppose we can think of this as having Hashable requirements implemented by members annotated with @_implements that have unutterable (and therefore unshadowable) names.

1 Like

So the key distinction (and the thing that keeps us from being boxed in later with arbitrary tuple conformances) is the fact that this is a conformance being implicitly applied to all tuples (with all Hashable elements), as opposed to one where the author is explicitly asking for the conformance by writing some syntax (which could, in the future, allow elements to witness requirements of the same name)?

I can get behind that, and it makes sense. @Alejandro, if my understanding is correct, can you have the proposal explicitly spell this out? Since this is the first time in the language where a tuple would be conforming to anything, I think having some of these details spelled out clearly will be important/helpful for future work in this area.

Yeah, in principle, if we later allowed conformances for specific tuple types (or we allowed a conformance to explicitly request conditional behavior for sets of types with one of the mechanisms Doug describes here), then any labeled members could conceivably be eligible to become witnesses at that point.

1 Like

@mcritz You can read a more detailed discussion of the current behavior here:

https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID70

@xwu thanks for the info. Iā€™ve learned something useful today.

The analogy is that we already have scenarios where a member resolves differently in concrete and generic contexts. But if you insist, hereā€™s one where the same thing happens for a protocol requirement:

protocol P {
  associatedtype T
  var x: T { get }
}
extension P where T: BinaryInteger { var x: T { 1 } }

struct S<T> {}
extension S: P where T: BinaryInteger {}
extension S where T == Int { var x: Int { 2 } }

func fooS(_ s: S<Int>) { print(s.x) }
func fooP<U: P>(_ u: U) { print(u.x) }

let s = S<Int>()
fooS(s)           // 2
fooP(s)           // 1

Thereā€™s also a much simpler example using classes, but it involves behavior Iā€™d like to see changed so Iā€™m only mentioning it for completeness:

protocol A { var x: Int { get } }
extension A { var x: Int { 1 } }
class B: A {}
class C: B { var x: Int { 2 } }

(I want C.x to be considered an override of B.x, and thus require the override keyword and be dispatched dynamically. That is not currently the case.)

ā€¢ ā€¢ ā€¢

Edit:

Hereā€™s a simpler struct example with no associatedtype in the protocol:

protocol P { var x: Int { get } }
extension P { var x: Int { 1 } }

struct S<T>: P {}
extension S where T: BinaryInteger { var x: Int { 2 } }
2 Likes

An empty hash(into:) is the right implementation for Void (or, indeed, for any unit type). This corresponds to an implementation of == that simply returns true.

5 Likes

The compiler will still emit the ABI conformance name that's either in the compatibility library for older Swift runtimes or in the current Swift runtime. Let's assume this feature ships with Swift 5.3, the Swift 5.3+ runtime will contain this conformance while only the Swift 5.0-Swift 5.2 compatibility libraries have an extra implementation that get linked to older executable Swift binaries. So we don't need to move this to a Swift 5.3 or 5.4 or etc. compatibility library because their runtime includes the conformance.

We can have this be fixed to a 6 arity, but there's really no point because we're still going to have to deal with the ABI additions whether it be 6 arity or infinite elements.

I was thinking more along the lines where tuple syntax is just sugar for a Tuple type just like c# . I love the idea of being able to back deploy a runtime fix like proposed but I donā€™t feel that tuple conformances warrant its use.

I'm not very familiar with some of the nomenclatures, and a bit lost while trying to follow the discussion on Hashable conformance upthread. Just for the sake of clarity, consider the following 2 tuples:

let foo = ("the answer", 42)
let bar = (dayOf: "the answer", hashValue: 42)

What would be the evaluation to the following 2 expressions?

bar.hashValue == 42
bar.hashValue == foo.hashValue

From the proposal text:

Comparing a tuple to a tuple works elementwise:

Look at the first element, if they are equal move to the second element. Repeat until we find elements that are not equal and compare them.

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

In the future when we have proper tuple extensions, is it possible to retain the proposed Comparable implementation, not implement it in the standard library, but have it overridable by user-implemented Comparable conformance?

As I understand it, foo.hashValue wouldn't even compile. It has no such member. Also, bar.hashValue would return 42.

However, if you did this (as far as I understand it):

func printHash<H: Hashable>(from instance: H) {
    print(instance.hashValue)
}

print(bar.hashValue) // prints the element whose name is hashValue
printHash(from: bar) // prints the actual hash of the tuple
1 Like

If this is the case, tuples wonā€™t conform to Hashable, but will be implicitly convertible to Hashable. That seems very unswifty to me.

1 Like

Nope. It will conform. But just as is already possible in Swift, a member can resolve to different implementations given different context. See multiple examples further up this thread.

1 Like