KeyPath collection issue where `Value` is an existential

I’m unable to solve this problem. I need a way to loop over a collection of key-path values but the compiler does not let me when the Value is an existentential or a protocol:

protocol A {}
protocol B {}

extension Int : A, B {}
extension String : A, B {}

struct Foo {
  let x: Int = 42
  let y: String = "swift"
}

let test: [KeyPath<Foo, A & B>] = [
  \.x, // Key path value type 'Int' cannot be converted to contextual type 'A & B'
  \.y
]

I also tried:

let test: [AnyKeyPath] = ....
let test2 = test.compactMap { $0 as? KeyPath<Foo, A & B> }

But test2 won’t contain any values.

Key paths are not covariant like this. You can have a collection of [PartialKeyPath<Foo>], though.

Is there no real way to workaround this issue? I really need it to be KeyPath<Foo, A & B>.

You can apply a PartialKeyPath to a base value and get a value of Any, and cast that to A & B.

If you really need this, you can define computed properties on the type itself:

extension Foo {
  fileprivate var x_AB: A & B { return self.x }
  fileprivate var y_AB: A & B { return self.y }
}

let test: [KeyPath<Foo, A & B>] = [\.x_AB, \.y_AB]

Or you can use closures instead of key paths:

let test: [(Foo) -> A & B] = [
  { $0.x },
  { $0.y }
]

(note: I didn’t test either of these code samples, so they may have small bugs)

3 Likes

That, or provide an extension property on the protocol composition:

extension A where Self: B {
  var asAB: A & B { return self }
}

which would let you use \.x.asAB and \.y.asAB. Note that these would no longer be equal to \.x and \.y, if that’s important.

5 Likes

I’d like to try that but I’m already hitting the next wall. One of the path that I need has a subscript which uses an enum as a key.

\BluetoothCharacteristic.cameraSetting[.iso] // Type of expression is ambiguous without more context

What I can do is this, but it’s really verbose and tedious:

let a = \BluetoothCharacteristic.cameraSetting
let b: PartialKeyPath<BluetoothCharacteristic> = a.appending(path: \.[.iso])

Is there a better way of expressing it one line?

That looks like a type-checker bug. Does it work if you explicitly name EnumName.iso?

No, but it seems I can silent the error by adding as PartialKeyPath<BluetoothCharacteristic> at the end. :face_with_head_bandage:

Weird. Does cameraSetting by chance come from a superclass of BluetoothCharacteristic? @rudkx fixed a bug related to that recently. If not, this is definitely worth a bug of its own.

No classes are involved here, but the types are a little complex:

extension CameraSettingCharacteristic {
  ///
  public struct Path {
    ///
    public subscript (_ kind: Kind) -> ORWCharacteristicContainer<
      CameraSettingCharacteristic, UInt8, Data
    > { ... }
  }
}

public struct BluetoothCharacteristic {
  public let cameraSetting = CameraSettingCharacteristic.Path()
}

If I can shrink it down to something small that reproduces the issue, I’ll file an issue tomorrow.

@Joe_Groff other then that, wouldn’t it make sense to make Key Paths also covariant like collections?

This worked for me as a workaround. That way the type information I need won’t be lost, thank youl