[Idea] Type erasure for protocols with Self or associated type requirements.


(Vatsal Manot) #1

(This is my first time on swift-evolution / Mailman, I apologize for any formatting errors)

As we know, the following fails:

protocol _Protocol

{

static var _Parameter: Any.Type { get }

var nonGenericVariable: Int { get }

var _parameter: Any { get set }

func inputParameter(: Any) -> Any?

}

Protocol.self

with the error:

protocol ‘Protocol’ can only be used as a generic constraint because it has Self or associated type requirements

For quite some time, I have been using a particular workaround for this:

protocol _Protocol

{

static var _Parameter: Any.Type { get }

var nonGenericVariable: Int { get }

var _parameter: Any { get set }

func inputParameter(: Any) -> Any?

}

extension _Protocol where Self: Protocol

{

static var _Parameter: Any.Type

{

return Parameter.self

}

var _parameter: Any

{

get

{

return parameter

}

set

{

parameter = newValue as! Parameter

}

}

func _inputParameter(parameter: Any) -> Any?

{

return (parameter as? Parameter).map(inputParameter)

}

}

protocol Protocol: _Protocol

{

typealias Parameter

var nonGenericVariable: Int { get }

var parameter: Parameter { get set }

func inputParameter(_: Parameter) -> Parameter

}

And it has worked well so far.

The idea is to let the compiler generate these type-erased ‘versions’ on demand, using syntax like:

Protocol.Opaque

The rough procedure of creating a type-erased version for a given protocol is as follows:

  • Create a protocol with an underscore prefixed to the name of the target protocol.

  • Expose all the non-ATD (self or associated type dependent) constructs as requirements (essentially just copying the declarations)

  • Expose all the ATD constructs as requirements with associated types replaced with “Any”, with an underscore prefixed to their name changing the return value of functions accepting ATD inputs to an optional version of the same type.

  • Extend the target protocol with implementations of these type-erased ‘versions’ of ATD constructs. For variables, a computed property is provided which returns the target variable, and force casts newValue in the setter (if any) to the required ATD type. For functions, the parameters undergo an attempted cast to the required ATD types, and are then mapped over the original implementation. Functions without ATD parameters can return just Any, because there is no casting involved.

  • Initializers become static functions with prefixed underscores, returning an Optional<Protocol.Opaque>

I admit that I don’t have a plan on how this should be implemented. My fear is that it require tremendous amounts of metadata, bloating the binary size, and so I have submitted this as an idea and not a proposal.

I have created a gist (https://gist.github.com/vmanot/888afc3f26caf142cd21) as a demonstration.

This pattern has helped on innumerable occasions, and is used in my projects to allow some runtime tricks. It also helps me forward implementations in a way similar (behavior-wise) to “AnyGenerator”

import Swift

struct AnyGenerator

{

var base: _GeneratorType

init<G: protocol<_GeneratorType, GeneratorType> where G.Element == Element>(base: G)

{

self.base = base

}

mutating func next() -> Element?

{

return (base._next() as! Optional)

}

}

AnyGenerator(base: [1, 2, 3].generate())

(I have tested this with an actual_GeneratorType implementation, and it works)

Please let me know what you think of this idea, and whether it is viable or not.