I'm trying to workaround missing identity key-path in Swift 4.2 with a custom protocol that adds a _self property. However when I create a partial key-path from within an extension of my other protocol it produces a key-path with an unexpected hash value which would always fail the equality test later on.
Am I doing something wrong or is this a weird bug?
prefix operator ^
prefix func ^ <Root, Value>(keyPath: KeyPath<Root, Value>) -> (Root) -> Value {
return { root in root[keyPath: keyPath] }
}
prefix func ^ <Root, Value>(
keyPath: KeyPath<Root, Value>
) -> PartialKeyPath<Root> {
return keyPath
}
protocol _Identity {
var _self: Self { get }
}
extension _Identity {
var _self: Self {
return self
}
}
protocol Configurator {
associatedtype Driver: _Identity
typealias PartialKeyPathSet = Set<PartialKeyPath<Driver>>
func configure(_ driver: inout Driver, at keyPaths: PartialKeyPathSet)
}
extension Configurator {
func configure(_ driver: inout Driver) {
#warning("FIXME: Remove the underscore in Swift 5.")
// WHY DOES THIS GENERATE SOMETHING UNEXPECTED?
configure(&driver, at: [^\._self])
}
}
extension Collection where Element: Equatable {
func containsAny<C>(
from other: C
) -> Bool where C: Collection, C.Element == Element {
return other.contains(where: self.contains)
}
}
struct S: _Identity, Hashable {
var isAvailable = false
var value = 42
}
struct Tester: Configurator {
typealias Driver = S
func configure(_ driver: inout Driver, at keyPaths: PartialKeyPathSet) {
let identifyKeyPath = \Driver._self
if keyPaths.containsAny(from: [^\._self, ^\.isAvailable]) {
driver.isAvailable = Bool.random()
print("works")
}
if keyPaths.contains(^\.value) {
driver.value = Int.random(in: 0 ... 10)
}
print("""
identify key path: \(identifyKeyPath)
identity key path hash value: \(identifyKeyPath.hashValue)
keyPaths: \(keyPaths)
keyPaths (hash values): \(keyPaths.map(^\.hashValue))
keyPaths contains identity key path: \(keyPaths.contains(identifyKeyPath))
""")
}
}
let tester = Tester()
var s = S()
tester.configure(&s, at: [^\._self])
tester.configure(&s) // This line will use the method from the extension.
tester.configure(&s, at: [^\._self])
The output of this looks always like this (I don't mean that the hash values remain during each run though):
This is a known issue; it's an unfortunate bit of the implementation model leaking out. The protocol requirement is an independent declaration from the extension method or any concrete implementations in concrete types, so a reference to \._self on the protocol type or a generic type constrained by the protocol, which will end up referencing the requirement, is considered not equal to \._self on the concrete type, which will end up referencing the implementation declaration.
I read your comment twice but I'm still not sure I fully follow. Are you saying this is an issues that was overseen during the original implementation and requires someone to fix it someday, or is it an unfixable artifact of key-paths? Do you have any workaround for this, as ugly it can be I'll take any?! In my particular case I will probably drop the extension method until I can write \.self, but I really don't know if I ever build anything similar somewhere else which would already result in unexpected behavior.
Should I file a bug still? (This issue is also present in Swift 5.)
Sorry my explanation wasn't clear. It's a bit like overload resolution. If you had the following declarations:
protocol P {}
extension String: P {}
func foo<T: P>(_: T) {}
func foo(_: String) {}
then from a generic context where you have a variable x: T where T: P, calling foo(x) would always pick the first overload of foo, whereas if you were in a context where x: String, calling foo(x) would pick the second. A similar situation exists between protocol requirements and their implementations. When you look up a protocol member on a generic type, overload resolution sees only the protocol requirements followed by extension members. On a concrete type, overload resolution sees only the implementations. Normally you don't notice this, because the runtime behavior works out the same and the identity of the decl chosen doesn't semantically matter, but key paths end up exposing this by making the identity of declarations impact equality and hashing.
This could conceivably be fixed by adjusting key path member lookup to always favor protocol requirements in the compiler. There's a bug open for this; I haven't had time yet to evaluate the impact of trying this.
There may in fact be a bug in that case. I would expect the _Identity.selfKeyPath function to return an equivalent key path when invoked on the same Self type.