Referencing different objects implementing a protocol where the protocol has 'associatedtype'


(Palfi, Andras) #1

Hi All,

Using protocols are quite important in case of “plugin” concept to implement delegation: allow others to implement small components which will be run in specific cases.
To support one component a delegate or closure is sufficient. For the delegate we can use the protocol as referenced type unless it has associated type. If it uses associated type we can still use Generics to reference the object and use protocol only for constraints.
Allowing more components to subscribe causes the problem that we cannot reference them:

· We cannot use generics as there can be any arbitrary number and type of objects so we have to use protocols to reference them

· When there is an associatedtype in the protocol it is not possible to use to reference objects

For example:
We have a generic class which holds a collection of some type of objects – the generic parameter is a type of an item.
This class exposes a kind of delegate (specified by a protocol) and arbitrary number of implementers can subscribe. These delegates will be called in a specific case: for example when a new item is added to the collection.
The defined protocol uses associatedtype since the specific type of element is unkown.
The problem that the code cannot compile because of the error: "protocol 'SomeProtocol' can only be used as a generic constraint because it has Self or associated type requirements"

So we know the specific element we want to store but still cannot specify it. We cannot use generic for the ‘delegates’ as it should contain different implementations of SomeProtocol.

protocol SomeProtocol {
    associatedtype ElementType

    func someMethod(withElement: ElementType) -> Void
}

class SomeProtocolImplInt : SomeProtocol {
    typealias ElementType = Int

    var sum : Int = 0

    func someMethod(withElement element: Int) {
        sum += element
    }
}

class Container<SomeElementType> {
    typealias ElementType = SomeElementType

    var elements = Array<SomeElementType>()

    var delegates = Array<SomeProtocol where SomeProtocol.ElementType==SomeElementType>() // ‘where’ could solve the problem but not allowed here

    func subscribe<D : SomeProtocol>(delegate : D) where D.ElementType==SomeElementType { // would be better without generics as this generates as many methods as many different types used to call it
        delegates.append(delegate)
    }

    func add(element: SomeElementType) {

        for delegate in delegates {
            delegate.someMethod(withElement: element)
        }
        elements.append(element)
    }
}

// usage:
var container = Container<Int>()
let calculator = SomeProtocolImplInt()
container.subscribe(delegate: calculator)
container.add(element: 1)

Workaround:
Use 'Thunks'. These are type eraser structs, which implements the same protocol. For each method they referencing (capturing) the original methods and properties of the object. So 'thunks' are technically proxy objects.

struct DirtyThunk<SomeElementType> : SomeProtocol {

    private let _someMethod : (SomeElementType) -> Void

    init<D : SomeProtocol>(delegate : D) where D.ElementType==SomeElementType {
        _someMethod = delegate.someMethod
    }

    func someMethod(withElement: SomeElementType) {
        _someMethod(withElement)
    }
}

the subscribe method will be modified:
func subscribe<D : SomeProtocol>(delegate : D) where D.ElementType==SomeElementType { // would be better without generics as this generates as many methods as many different types used to call it
        let thunk = DirtyThunk(delegate)
        delegates.append(thunk)
    }

This solution works - however we can never retrieve the original object any more as it is not referenced. The implementation of the “thunks” are also painful a bit. The methods are captured only by the name of the methods without the parameters so leads the problem if different methods have the same prefix.

I tried to solve using ‘Any’ to reference the delegates but then cannot cast to a proper type to call them.

Do one know any better solution?

Thanks,
Andras


(Brent Royal-Gordon) #2

There's a more complex way to write a "type-erased wrapper" (the preferred Swift term for a thunk). The trick is that you use a base class which is *not* generic on the actual type, plus a derived class which *is* generic on the actual type, to hold the actual instance. For example:

  private class SomeProtocolBox<SomeElementType> {
    private init() {}
    
    func someMethod(withElement: SomeElementType) { fatalError() }
  }
  
  private class ConcreteSomeProtocolBox<SomeProtocolType: SomeProtocol>: SomeProtocolBox<SomeProtocolType.ElementType> {
    var value: SomeProtocolType
    
    init(_ value: SomeProtocolType) {
      self.value = value
    }
    
    override func someMethod(withElement: SomeElementType) {
      value.someMethod(withElement: withElement)
    }
  }
  
  public struct AnySomeProtocol<SomeElementType>: SomeProtocol {
    public typealias ElementType = SomeElementType
    
    private var box: SomeProtocolBox<SomeElementType>
    
    public init<T: SomeProtocol>(_ value: T) where T.ElementType == SomeElementType {
      box = ConcreteSomeProtocolBox(value)
    }
    
    func someMethod(withElement: SomeElementType) {
      box.someMethod(withElement: withElement)
    }
  }

With this in place, it's not difficult to support recovering the original instance as an `Any`:

  extension SomeProtocolBox {
    var base: Any { fatalError() }
  }
  extension ConcreteSomeProtocolBox {
    override var base: Any { return value }
  }
  extension AnySomeProtocol {
    var base: Any { return box.base }
  }

It's also more efficient than the closure-based solution, since it doesn't need a separate closure (and context) for each method it indirects.

There's some hope that eventually all of this will be unnecessary and you'll be able to use protocols with associated types in more places, possibly with some special casting syntax to make sure you're passing the proper type. No idea if or when that will actually be implemented, though.

···

On Sep 18, 2016, at 6:21 AM, Palfi, Andras via swift-users <swift-users@swift.org> wrote:

Workaround:
Use 'Thunks'. These are type eraser structs, which implements the same protocol. For each method they referencing (capturing) the original methods and properties of the object. So 'thunks' are technically proxy objects.

struct DirtyThunk<SomeElementType> : SomeProtocol {

   private let _someMethod : (SomeElementType) -> Void

   init<D : SomeProtocol>(delegate : D) where D.ElementType==SomeElementType {
       _someMethod = delegate.someMethod
   }

   func someMethod(withElement: SomeElementType) {
       _someMethod(withElement)
   }
}

the subscribe method will be modified:
func subscribe<D : SomeProtocol>(delegate : D) where D.ElementType==SomeElementType { // would be better without generics as this generates as many methods as many different types used to call it
       let thunk = DirtyThunk(delegate)
       delegates.append(thunk)
   }

This solution works - however we can never retrieve the original object any more as it is not referenced. The implementation of the “thunks” are also painful a bit. The methods are captured only by the name of the methods without the parameters so leads the problem if different methods have the same prefix.

I tried to solve using ‘Any’ to reference the delegates but then cannot cast to a proper type to call them.

Do one know any better solution?

--
Brent Royal-Gordon
Architechies