I ran into somewhat unexpected behavior when trying to compare key paths for equality. My best guess is that the key path that I'm passing into the doSomething(with:) method references the concrete type's implementation, and that even though that implementation is a witness to a protocol requirement, it produces a different key path under the hood than referencing the property via an existential. Any clarification would be much appreciated!
This is the output when I run the demo() function:
C Array<Int>
keyPath \Array<Int>.count
\C.count \Array<Int>.count
keyPath == \C.count false
type(of: keyPath) KeyPath<Array<Int>, Int>
type(of: \C.count) KeyPath<Array<Int>, Int>
are key paths equal false
func demo() {
let array = [[1, 2, 3]]
array.doSomething(with: \.count)
}
extension Array {
func doSomething<Value>(with keyPath: KeyPath<Element, Value>) {
func keyPath_is_count<C: Collection>(_: C.Type) -> Bool {
print("C", C.self)
print("keyPath", keyPath)
print("\\C.count", \C.count)
print("keyPath == \\C.count", keyPath == \C.count)
print("type(of: keyPath)", type(of: keyPath))
print("type(of: \\C.count)", type(of: \C.count))
return keyPath == \C.count
}
if let collectionType = (Element.self as? any Collection.Type) {
print("are key paths equal", keyPath_is_count(collectionType))
}
}
}
1 Like
The problem is observed even without introducing existential. I removed if let collectionType = (Element.self as? any Collection.Type) and made extension Array Element to be a Collection
extension Array where Element: Collection { // now explicitly a collection without casting Element.Type to existential
func doSomething<Value>(with keyPath: KeyPath<Element, Value>) {
func keyPath_is_count<C: Collection>(_: C.Type) -> Bool {
return keyPath == \C.count
}
print("are key paths equal", keyPath_is_count(Element.self))
}
}
let array = [[1, 2, 3]]
array.doSomething(with: \.count)
1 Like
I've dome some inspection:
\Array<Int>.count == \Array<Int>.count // true | as expected
\Array<Array<Int>>[0].count == \Array<Array<Int>>[0].count // true | as expected
\Array<Array<Int>>[0].count == \Array<Array<Int>>[1].count // false | as expected
let keyPath0: KeyPath<Array<Array<Int>>, Int> = \Array<Array<Int>>[0].count
let keyPath1: KeyPath<Array<Array<Int>>, Int> = \Array<Array<Int>>[1].count
let keyPathC: KeyPath<Array<Array<Int>>, Int> = \Array<Array<Int>>.count
// As we see Type of keyPath0 , keyPath1 and keyPathC is the same.
// However, if we print debug description, piece of underlying data is reflected
// and we see that these keyPath instances differs.
// We can't see what is the difference between keyPath0 and keyPath1,
// but at least we see the difference between keyPathC
let a = String(reflecting: keyPath0) // "\\Array<Array<Int>>.subscript(_: Int).count"
let b = String(reflecting: keyPath1) // "\\Array<Array<Int>>.subscript(_: Int).count"
let c = String(reflecting: keyPathC) // "\\Array<Array<Int>>.count"
As a next step I modified the code snippet:
extension Array where Element: Collection {
func doSomething<Value>(with keyPath: KeyPath<Element, Value>) {
func keyPath_is_count<C: Collection>(_: C.Type) -> Bool {
let fromArg: KeyPath<Self.Element, Value> = keyPath
let fromType: KeyPath<C, Int> = \C.count
let fromArgDescr = String(reflecting: fromArg) // "\\Array<Int>.count"
let fromTypeDdescr = String(reflecting: fromType) // "\\Array<Int>.count"
return keyPath == \C.count
}
There are no differences seen between fromArg and fromType.
There are also no subscript indices introduced in KeyPath metadata.
There is also no difference between fromArg and fromType in mental model – both key paths constructed from the same Element.Type (aka Array<Int>)
Seems to be a bug.