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.
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:
-
Tuple's
x
is a witness forP.x
, which differs from the synthesizedhashValue
behavior described here -
Tuple's
x
is not a witness forP.x
, which makes the conformance brittle/a huge footgun, because it will behave differently depending on whether the tuple is passed asP
or<T: P>
-
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 ahashValue
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.
@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.
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.
I suppose we can think of this as having Hashable
requirements implemented by members annotated with @_implements
that have unutterable (and therefore unshadowable) names.
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.
@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 } }
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.
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
If this is the case, tuples wonāt conform to Hashable
, but will be implicitly convertible to Hashable
. That seems very unswifty to me.
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.