0

As KeyPath can't be casted to super types in Swift, I want to write a type-erased version, which represents any KeyPath whose value could be casted to a specific protocol or super type:

public struct PropertyGetter<Root, ValueType> {
  private let keyPath: PartialKeyPath<Root>

  public init<T: ValueType>(_ keyPath: KeyPath<Root, T>) {
    self.keyPath = keyPath
  }

  public func get(_ instance: Root) -> ValueType {
    return instance[keyPath: self.keyPath] as! ValueType
  }
}

The compiler rightfully complains that

type 'T' constrained to non-protocol, non-class type 'ValueType.Type'

as ValueType could potentially be a struct type.

So how do we properly constrain this? That is enforcing that

  1. ValueType is either a class type or a protocol, i.e subclassable / implementable
  2. T is constrained to conform to ValueType, i.e x as! ValueType must be guranteed to work, where x: T

Notice that it is indeed possible to write such a type-erasing struct, when the Protocol type is fixed. E.g a class which only accepts KeyPaths pointing to CustomStringConvertible members:

public struct CustomStringConvertibleGetter<Root> {
  private let keyPath: PartialKeyPath<Root>

  public init<T: CustomStringConvertible>(_ keyPath: KeyPath<Root, T>) {
    self.keyPath = keyPath
  }

  public func get(_ instance: Root) -> CustomStringConvertible {
    return instance[keyPath: self.keyPath] as! CustomStringConvertible
  }
}

let getter1 = CustomStringConvertibleGetter(\SomeClass.someString) // works
let getter2 = CustomStringConvertibleGetter(\SomeClass.nonConformingMember) // will throw an error at compile time
Sebastian Hoffmann
  • 2,815
  • 1
  • 12
  • 22
  • It seems like you misunderstand type erasure. Once you type erase something, it loses its original type information, so you won't be able to cast it back to its original type. Moreover, your init cannot work, since what you are trying to achieve in the `init` is exactly what you describe that cannot work: storing a keypath of a different type in a variable. What you are trying to achieve isn't possible, since `KeyPath` is a generic type and [Generic types are covariant in Swift](https://stackoverflow.com/questions/41976844/swift-generic-coercion-misunderstanding). Type erasure won't solve that – Dávid Pásztor Nov 09 '19 at 16:52
  • @DávidPásztor Maybe I expressed myself wrongly but please consider the above example where `KeyPath` is replaced by `KeyPath` (dropping `T` altogether ofc). Don't you agree that then the above code should work? The "erased-type" of the original keypath is actually stored statically in the generic type parameter `ValueType`. Now all I want to achieve is to gurantee that `T` conforms to `ValueType`. This makes sure that that cast in the `get()` method succeeds because if the `Any` object can be casted to `T`, then it can be casted to `ValueType` as well. – Sebastian Hoffmann Nov 09 '19 at 17:02
  • @DávidPásztor I added a proof-of-concept showing that the type-erasure works properly when the Protocol type is known beforehand. This is not a problem regarding type-erasure specifically but rather regarding the generic/type-system overall. – Sebastian Hoffmann Nov 09 '19 at 18:06
  • E.g c++ templates can easily implement this, as they just substitute template-parameter (i.e *SFINAE*, see https://en.cppreference.com/w/cpp/types/is_base_of) – Sebastian Hoffmann Nov 09 '19 at 18:12
  • 1
    The Swift type system can't express this kind of covariance currently. See https://bugs.swift.org/browse/SR-5213 It's a fine thing to ask for, it's just not currently possible. You can't constrain generic type parameters to be subtypes of other generic type parameters. You'll need to redesign your system to avoid this requirement. – Rob Napier Nov 09 '19 at 22:08

0 Answers0