Allow protocols with sufficient same-type constraints to be used as a regular type

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

This has been discussed multiple times before. An implementation and proposal are already in progress:

2 Likes
Terms of Service

Privacy Policy

Cookie Policy