KeyPath equality issue where `Root` is a generic type


(Will Lisac) #1

I'm having an issue with comparing KeyPaths where one KeyPath's Root is a generic type.

For example:

protocol Named {
    var name: String { get set }
}

struct Person: Named {
    var name: String
}

struct Container<T: Named> {
    let nameKeyPath: WritableKeyPath<T, String> = \T.name
}

let container = Container<Person>()
let keyPath1: WritableKeyPath<Person, String> = container.nameKeyPath
let keyPath2: WritableKeyPath<Person, String> = \Person.name

// returns false, was expecting it to be true
keyPath1 == keyPath2

I'm not entirely sure why these KeyPaths aren't considered equal. Any advice / thoughts would be appreciated. :slight_smile:

As a workaround, I've found that using the protocol as the Root for both KeyPaths makes the KeyPaths equal.

For example:

protocol Named {
    var name: String { get set }
}

struct Person: Named {
    var name: String
}

struct Container<T: Named> {
    let nameKeyPath: WritableKeyPath<Named, String> = \T.name
}

let container = Container<Person>()
let keyPath1: WritableKeyPath<Named, String> = container.nameKeyPath
let keyPath2: WritableKeyPath<Named, String> = \Person.name

// returns true
keyPath1 == keyPath2

I've read this post, and it sounds similar, but a little different: KeyPath collection issue where `Value` is an existential


(Huon Wilson) #2

(Very) Slightly simplified:

protocol Named {
    var name: String { get set }
}

struct Person: Named {
    var name: String
}

func makeIt<T: Named>(_: T.Type) -> WritableKeyPath<T, String> {
    return \T.name
}

let keyPath1: WritableKeyPath<Person, String> = makeIt(Person.self)
let keyPath2: WritableKeyPath<Person, String> = \Person.name
// returns false, was expecting it to be true
print(keyPath1 == keyPath2)

Possibly a mismatch between how protocol requirements and concrete values are represented, @Joe_Groff?


(Joe Groff) #3

It's more of a language model issue. The protocol declaration is not really the same declaration as the concrete declaration inside Person; when you declare conformance, a correspondence is established between Person::name and Named::name, but they're still considered distinct. Looking up name inside Person favors the former because it's more specific, but there's no way to explicit reference Named::name on a concrete Person. You could work around this by providing a protocol extension that vends the key path for the protocol declaration:

extension Named {
  var nameKey: WritableKeyPath<T, String> { return \Self.name }
}

(Will Lisac) #4

Thanks! That appears to work.

The results still seem a bit unintuitive to me, but I think I better understand what's going on now.

Is this the desired behavior, or is this something that could/should changed in a future release?


(Joe Groff) #5

Yeah, I agree it's confusing. It's worth filing a bug report to improve this.


(Matthew Johnson) #6

What kind of improvement do you have in mind? Making it easier to get the key path for a conformance or something else?


(Joe Groff) #7

At compile time, when we know up front that a type conforms to a protocol in its defining module, it'd be nice to collapse the "selectors" for the implementation and requirement so that referencing one or the other is exactly equivalent. This would also provide some resilience for public types, since it would make it a non-breaking change for a type to switch between implementing a protocol using its own method or an extension method in all cases (cc @Slava_Pestov). There'd still be issues with retroactive conformances, since in some contexts the conformance isn't known.

Dynamically, if there were enough information in a protocol witness table to identify the concrete implementation, the key path runtime could conceivably use that to equate a key path that refers to the concrete implementation with one that refers to the requirement only.


(Mixio) #8

I stumbled across the same issue storing a keyPath in a generic type.

// Unexpected inequality of a keyPath formed through "generic parameter type with constraint" and that same keyPath, manually formed.
// Please see code below.
// Both keyPaths point to the same property of the same root type.
// Both keyPaths retreive the same value through keypath subscript.
// But they are not equal and their hashValue is not equal.
//
// Workaround: Trampoline the keyPath.

protocol Named {
    var name: String { get set }
    static var trampolinedKeyPath: WritableKeyPath<Self, String> { get }
}

struct A<T> where T: Named {
    var keyPath = \T.name
    var trampolinedKeyPath = T.trampolinedKeyPath
}

struct B: Named {
    var name: String = "Foo"
    static var trampolinedKeyPath = \B.name
}

let a = A<B>()
let b = B()

print("\\B.name                            // \(\B.name)")
print("a.keyPath                          // \(a.keyPath)")
print("a.trampolinedKeyPath               // \(a.trampolinedKeyPath)")
print("(\\B.name == a.keyPath)             // \(\B.name == a.keyPath) <----------------------=== UNEXPECTED!")
print("(\\B.name == a.trampolinedKeyPath)  // \(\B.name == a.trampolinedKeyPath)  <----------------------=== CORRECT!")
print("(\\B.name).hashValue                // \((\B.name).hashValue)")
print("a.keyPath.hashValue                // \(a.keyPath.hashValue)")
print("a.trampolinedKeyPath.hashValue     // \(a.trampolinedKeyPath.hashValue)")

print("b[keyPath: \\B.name]                // \(b[keyPath: \B.name])")
print("b[keyPath: a.keyPath]              // \(b[keyPath: a.keyPath])")
print("b[keyPath: a.trampolinedKeyPath]   // \(b[keyPath: a.trampolinedKeyPath])")

/*
 \B.name                            // Swift.WritableKeyPath<__lldb_expr_106.B, Swift.String>
 a.keyPath                          // Swift.WritableKeyPath<__lldb_expr_106.B, Swift.String>
 a.trampolinedKeyPath               // Swift.WritableKeyPath<__lldb_expr_106.B, Swift.String>
 (\B.name == a.keyPath)             // false <----------------------=== UNEXPECTED!
 (\B.name == a.trampolinedKeyPath)  // true  <----------------------=== CORRECT!
 (\B.name).hashValue                // -7233830513210056797
 a.keyPath.hashValue                // 8219701636820799123
 a.trampolinedKeyPath.hashValue     // -7233830513210056797
 b[keyPath: \B.name]                // Foo
 b[keyPath: a.keyPath]              // Foo
 b[keyPath: a.trampolinedKeyPath]   // Foo
 */