Hello everyone.
As of right now, a protocol, inherited from protocol(s) with associated types, with sufficient same-type constraints cannot be used as a type.
Consider the following example:
protocol Stateful {
associatedtype State
var state: State? { get }
mutating func restore(state: State)
}
protocol Model: Stateful {
associatedtype Configuration
mutating func setup(_ configuration: Configuration)
}
protocol AnyModelProtocol: Model where State == AnyState, Configuration == AnyConfiguration {}
func test() {
let anyModel: AnyModelProtocol? = nil
}
Now it produces the error
Protocol 'AnyModel' can only be used as a generic constraint because it has Self or associated type requirements
even though all of the associated types are resolved.
This imposes a limitation when implementing type-erasure.
Right now there are two options to do it:
Implement it with class inheritance
Something like this
struct AnyModel: Model {
private let base: AnyModelBase
init<Base: Model>(_ base: Base) {
self.base = AnyModelConcrete(base)
}
}
extension AnyModel: Model {
typealias Configuration = AnyConfiguration
typealias State = AnyState
func setup(_ configuration: AnyConfiguration) {
base.setup(configuration)
}
var state: AnyState? {
base.state
}
func restore(state: AnyState) {
base.restore(state: state)
}
}
private class AnyModelBase: Model {
typealias Configuration = AnyConfiguration
typealias State = AnyState
func setup(_ configuration: AnyConfiguration) {}
var state: AnyState?
func restore(state: AnyState) {}
}
private class AnyModelConcrete<Base: Model>: AnyModelBase {
let base: Base
init(_ base: Base) {
self.base = base
}
override func setup(_ configuration: AnyConfiguration) {
base.setup(configuration.unwrap())
}
override func restore(state: AnyState) {
base.restore(state: state.unwrap())
}
override var state: AnyState? {
base.state.map(AnyState.init)
}
}
But there is a problem with that approach - it uses heap allocations even though wrapped type may be a value type
So this does not always a good thing, but there is another approach, which is used to implement AnyHashable
in particular
Protocol + struct type-erasure
Basically AnyHashable
implementation introduces another protocol which has no Self uses, so it can be used as a type and a generic struct
, which implements this protocol.
Type erasure for the example above could be written in this manner as well
struct AnyModel {
private let base: ModelBox
init<Base: Model>(_ base: Base) {
self.base = ModelBoxConcrete(base: base)
}
}
extension AnyModel: Model {
typealias Configuration = AnyConfiguration
typealias State = AnyState
mutating func setup(_ configuration: AnyConfiguration) {
base.setup(configuration)
}
var state: AnyState? {
base.state
}
mutating func restore(state: AnyState) {
base.restore(state: state)
}
}
private protocol ModelBox {
var state: AnyState? { get }
func restore(state: AnyState)
func setup(_ configuration: AnyConfiguration)
}
private struct ModelBoxConcrete<Base: Model> {
let base: Base
}
extension ModelBoxConcrete: ModelBox {
var state: AnyState? {
base.state.map(AnyState.init)
}
func restore(state: AnyState) {
guard let state: Base.State = state.unwrap() else { return }
base.restore(state: state)
}
func setup(_ configuration: AnyConfiguration) {
guard let configuration: Base.Configuration = configuration.unwrap() else { return }
base.setup(configuration)
}
}
But this approach is somewhat problematic too. It works with something like Hashable
because the Hashable
protocol is small
But if you want to make type-erased version of a bigger protocol - you have to copy-paste a lot of code.
And, moreover, if someday you want to add another protocol requirement to the protocol, you need to copy-paste it to another one.
If swift allowed AnyModelProtocol
to be used as a type - it would be possible to write multiple type-erased Model
-wrappers, which automatically inherit Model conformance as well
E.g.
struct LazyModel<T, Base: Model> {
let data: T
let transform: (T) -> Base
let model: Base?
init(data: T, transform: @escaping (T) -> Base) {
self.data = data
self.transform = transform
}
}
extension LazyModel: AnyModelProtocol {
mutating func setup(_ configuration: AnyConfiguration) {
guard let configuration: Base.Configuration = configuration.unwrap() else { return }
createModelIfNeeded()
model?.setup(configuration)
}
var state: AnyState? {
model?.state.map(AnyState.init)
}
mutating func restore(state: AnyState) {
guard let state: Base.State = state.unwrap() else { return }
createModelIfNeeded()
model?.restore(state: state)
}
}
private extension LazyModel {
mutating func createModelIfNeeded() {
if model == nil {
model = transform(data)
}
}
}
I would like to know what do you think about the idea