Implement generic method from protocol in a generic class


(Joanna Carter) #1

I have a protocol, used to erase the generic parameters:

protocol PropertyBagProtocol
{
  func getValue<ownerT, valueT>(for keyPath: PartialKeyPath<ownerT>) -> valueT
  
  func setValue<ownerT, valueT>(_ value: valueT, for keyPath: PartialKeyPath<ownerT>)
}

Then I have the "main part" of a generic class:

class PropertyBag<ownerT>
{
  private var owner: ownerT
  
  init(for owner: ownerT)
  {
    self.owner = owner
  }
  
  lazy var properties: [PartialKeyPath<ownerT> : PropertyProtocol] =
  {
    var properties = [PartialKeyPath<ownerT> : PropertyProtocol]()
    
    … // other code
    
    return properties
  }()
}

Then I have an extension to the generic class, to implement the type-erasing protocol:

extension PropertyBag : PropertyBagProtocol
{
  func getValue<ownerT, valueT>(for keyPath: PartialKeyPath<ownerT>) -> valueT
  {
    do
    {
      return try properties[keyPath]?.getValue() *** error
    }
    catch
    {
      fatalError()
    }
  }
  
  func setValue<ownerT, valueT>(_ value: valueT, for keyPath: PartialKeyPath<ownerT>)
  {
    do
    {
      try properties[keyPath]?.setValue(value) *** error
    }
    catch
    {
      fatalError()
    }
  }
}

The error is: Cannot subscript a value of type '[PartialKeyPath<ownerT> : PropertyProtocol]' with an index of type 'PartialKeyPath<ownerT>'

If I redefine the generic methods to avoid the same generic parameter name:

protocol PropertyBagProtocol
{
  func getValue<ownerType, valueT>(for keyPath: PartialKeyPath<ownerType>) -> valueT
  
  func setValue<ownerType, valueT>(_ value: valueT, for keyPath: PartialKeyPath<ownerType>)
}

… then this just gives the same errors with the different parameter name:

extension PropertyBag : PropertyBagProtocol
{
  func getValue<ownerType, valueT>(for keyPath: PartialKeyPath<ownerType>) -> valueT
  {
    do
    {
      return try properties[keyPath]?.getValue() *** error
    }
    catch
    {
      fatalError()
    }
  }
  
  func setValue<ownerType, valueT>(_ value: valueT, for keyPath: PartialKeyPath<ownerType>)
  {
    do
    {
      try properties[keyPath]?.setValue(value) *** error
    }
    catch
    {
      fatalError()
    }
  }
}

Error is now: Cannot subscript a value of type '[PartialKeyPath<ownerT> : PropertyProtocol]' with an index of type 'PartialKeyPath<ownerType>'

If I insert the following guard clause to cast from the method's generic parameter type to the class's generic parameter type:

    …
    
    guard let keyPath = keyPath as? PartialKeyPath<ownerT> else
    {
      fatalError()
    }
    
    …

That solves the error but I then get the following warning: Cast from 'PartialKeyPath<ownerType>' to unrelated type 'PartialKeyPath<ownerT>' always fails.

Except that, if I ignore the warning and run the code, the cast succeeds perfectly :wink:

I have seen a similar false positive when casting generic types in other situations but I would really appreciate it if anyone has got any ideas how to go about implementing a generic method from a non-generic protocol in a generic class that uses the same parameter type.


#2

If you rename your generic parameters so they are unique, then the error will instead say, eg. “Cannot subscript a value of type '[PartialKeyPath<E> : PropertyProtocol]' with an index of type 'PartialKeyPath<F>'”

Edit:

I tried copying your code into a playground, and it gave many other errors besides the one you mentioned. Can you please provide a minimal example?


(Joanna Carter) #3

Yes, I know. I demonstrated that in the second half of my post


(Garth Snyder) #4

Only because ownerType happens to be the same as ownerT in your test cases. PropertyBagProtocol as you've defined it requires you to implement getValue and setValue for every possible ownerType and valueT, which I suspect isn't really what you intended.

What is the effect you are trying to achieve?


(Anthony Latsis) #5

Why are you allowing to pass any PartialKeyPath to this instance method if the Key type of your properties dictionary is exactly PartialKeyPath<ownerT>? You probably don't want anyone to attempt getting a value for, say, a PartialKeyPath<String> on a property bag that stores PartialKeyPath<Bool>.


(Joanna Carter) #6

The PartialKeyPath<ownerT> will always hold the same type as it is refers to the type of a single class/struct. The valueT parameter will vary, depending on the type of the property referenced by the keyPath.

Hence, I am using PartialKeyPath<ownerT>, not KeyPath<ownerT, valueT>.

I am only holding all the keyPaths for a given type, not all possible types.


(Anthony Latsis) #7

Sorry if I wasn't clear enough, I'll rephrase. PartialKeyPath<ownerT> and PartialKeyPath< ownerType> can be completely different types, hence the subscripting error: what if you tried to subscript a dictionary of PartialKeyPath<String> with PartialKeyPath<Bool>?


(Joanna Carter) #8

Hi Garth.

Please ignore this part of my post about the casting warning. I had forgotten that this is due to ownerT being constrained to a combined type (Object & KeyPathDeclaration). This is a known bug in Swift that I had forgotten in trying to solve the rest of my problem.


(Joanna Carter) #9

Hi Anthony

As I just said, ownerT is guaranteed to always be the same in a PropertyBag, because all the properties belong to the same "owning" type.

I have tried to reduce the code to a bare minimum, compilable example:

protocol PropertyBagProtocol
{
  func test<ownerT>(for keyPath: PartialKeyPath<ownerT>)
}

class PropertyBag<ownerT>
{
  private var owner: ownerT
  
  init(for owner: ownerT)
  {
    self.owner = owner
  }
  
  lazy var properties: [PartialKeyPath<ownerT> : Any] =
  {
    var properties = [PartialKeyPath<ownerT> : Any]()
    
    // …
    
    return properties
  }()
}

extension PropertyBag : PropertyBagProtocol
{
  func test<ownerType>(for keyPath: PartialKeyPath<ownerType>)
  {
    guard let _ = properties[keyPath] else
    {
      fatalError()
    }
  }
}

The essence of the problem stems from the need to have a generic method (test<ownerT>) on a non-generic protocol (PropertyBagProtocol), implemented in a generic class (PropertyBag<ownerT>), whilst ensuring that the ownerT from the protocol's method is the same as the ownerT from the generic class.


(Anthony Latsis) #10

The point is that PartialKeyPath<ownerT> is not equivalent to PartialKeyPath<ownerType>, because ownerT and ownerType are unrelated generic parameters. Consider a much simpler example:

class PropertyBag<T> {
  var dictionary: [PartialKeyPath<T> : Any]

  func test<R>(for keyPath: PartialKeyPath<R>)
  {
    dictionary[keyPath] 
    // I cannot do this because R and T are unrelated.
  }
}

(Joanna Carter) #11

Yes, unfortunately, I know this only too well :face_with_raised_eyebrow:

In the end, I resorted to the following:

class PropertyBag<ownerT>
{
  private var owner: ownerT
  
  init(for owner: ownerT)
  {
    self.owner = owner
  }
  
  lazy var properties: [AnyKeyPath : PropertyProtocol] =
  {
    var properties = [AnyKeyPath : PropertyProtocol]()
    
    let keyPaths = ownerT.keyPaths
    
    for keyPath in keyPaths
    {
      if let valueType = type(of: keyPath).valueType as? PropertyFactory.Type
      {
        properties[keyPath] = valueType.createProperty()
      }
    }
    
    return properties
  }()
}

I can then make life simpler in the implementing methods:

extension PropertyBag : PropertyBagProtocol
{
  func getValue<ownerType, valueT>(for keyPath: PartialKeyPath<ownerType>) -> valueT
  {
    guard let property = properties[keyPath] else
    {
      fatalError()
    }
    
    do
    {
      return try property.getValue()
    }
    catch
    {
      fatalError()
    }
  }

Since I am responsible for creating the PartialKeyPath<ownerT> instances myself and calling the propertyBag from a strictly type owner, there is no way (hopefully) a "user" can get it wrong :wink:


(Anthony Latsis) #12

Was an associated type causing problems?

protocol PropertyBagProtocol {
  associatedtype ownerT
  func test<valueT>(for keyPath: PartialKeyPath<ownerT>) -> valueT
}

extension PropertyBag : PropertyBagProtocol {
  func test<valueT>(for keyPath: PartialKeyPath<ownerT>) -> valueT {
    guard let property = properties[keyPath] else {
      fatalError()
    }
    ...
  }
}

(Joanna Carter) #13

Oh yes :open_mouth: It automatically precludes using it in all sorts of places:

protocol PropertyBagFactory
{
  func createPropertyBag() -> PropertyBagProtocol *** error
}

error - Protocol 'PropertyBagProtocol' can only be used as a generic constraint because it has Self or associated type requirements.

I really wish somebody in charge of Swift would do something to sort this restriction out :roll_eyes:


(Anthony Latsis) #14

It can be frustrating indeed. You can dodge this one though

protocol PropertyBagFactory
{
  func createPropertyBag<T: PropertyBagProtocol>() -> T
}

(Joanna Carter) #15

I'll remember that for another time but, unfortunately, it doesn't work in this case:

public protocol DefaultValueProvider
{
  init()
}

protocol PropertyFactory : DefaultValueProvider
{
  static func createProperty() -> PropertyProtocol
}

extension PropertyFactory
{
  static func createProperty() -> PropertyProtocol
  {
    return Property<Self>()
  }
}

extension Int : PropertyFactory { }

extension Double : PropertyFactory { }

extension String : PropertyFactory { }

(Anthony Latsis) #16

You haven't mentioned what PropertyProtocol and Property are.


(Joanna Carter) #18
public protocol PropertyProtocol
{
  func getValue<valueType>() throws -> valueType
  
  func setValue<valueType>(_ value: valueType) throws
}

public class Property<valueT : DefaultValueProvider>
{
  public var value = valueT()
  
  public required init(value: valueT = valueT())
  {
    self.value = value
  }
}

extension Property : PropertyProtocol
{
  public enum Error : Swift.Error
  {
    case invalidPropertyType
  }
  
  public func getValue<valueType>() throws -> valueType
  {
    guard let value = value as? valueType else
    {
      throw Error.invalidPropertyType
    }
    
    return value
  }
  
  public func setValue<valueType>(_ value: valueType) throws
  {
    guard let newValue = value as? valueT else
    {
      throw Error.invalidPropertyType
    }
    
    self.value = newValue
  }
}

(Anthony Latsis) #19

I used the associated type and managed to compile everything expect the following line. Before we move on, are there any other errors on your side? I'm also not entirely sure I correctly assembled the code from the pieces scattered throughout this thread; if any issues remain, consider pasting the whole thing so I can follow.

func getValue<valueT>(for keyPath: PartialKeyPath<ownerT>) -> valueT {
    _ = properties[keyPath]//.getValue() 
    fatalError()
}

(Joanna Carter) #20

If I use the associated type as you suggest, then I end up with a few errors here:

open class Object
{
  private var _propertyBag: PropertyBagProtocol? ***error 1
  
  var propertyBag: PropertyBagProtocol ***error 1
  {
    get
    {
      if _propertyBag == nil
      {
        _propertyBag = (self as? PropertyBagFactory)?.createPropertyBag() ***error 2
        
        // …
      }

      return _propertyBag!
    }
    set
    {
        // …

      _propertyBag = newValue

        // …
    }
  }
}
  • error 1 - Protocol 'PropertyBagProtocol' can only be used as a generic constraint because it has Self or associated type requirements
  • error 2 - Cannot assign value of type '_?' to type 'PropertyBagProtocol?'

The problem is that Object is a base class and I can't pass its type to a generic PropertyBag<ownerT> as Self. That won't compile either as Self cannot be used there.

Here is an example of a class derived from Object:

final class Person : Object, KeyPathDeclaration
{
  var name: String
  {
    get
    {
      return propertyBag.getValue(for: \Person.name)
    }
    set
    {
      propertyBag.setValue(newValue, for: \Person.name)
    }
  }
  
  static var keyPaths: [PartialKeyPath<Person>]
  {
    return [\Person.name]
  }
}

(Anthony Latsis) #21

Is this acceptable? I've merged PropertyBagProtocol into PropertyBag here; are you absolutely sure you need the former, i.e. do you plan to have more than one conforming PropertyBag type?

protocol PropertyBagFactory {
  func createPropertyBag<T>() -> PropertyBag<T>
}

open class Object<ownerT> {
  private var _propertyBag: PropertyBag<ownerT>?
  
  var propertyBag: PropertyBag<ownerT> {
    get {
      if _propertyBag == nil {
        _propertyBag = (self as? PropertyBagFactory)?.createPropertyBag()
      }
      return _propertyBag!
    }
    set { _propertyBag = newValue }
  }
}

final class Person : Object<Person> {
  var name: String {
    get { return propertyBag.getValue(for: \Person.name) }
    set { propertyBag.setValue(newValue, for: \Person.name) }
  }
  
  static var keyPaths: [PartialKeyPath<Person>] {
    return [\Person.name]
  }
}