Checking equality of key paths not working as I expected

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.