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.