How to make protocol associated type require protocol inheritance and not protocol adoption

1.7k views Asked by At

In my swift project I have a case where I use protocol inheritance as follow

protocol A : class{

}

protocol B : A{

}

What Im trying to achieve next is declaring another protocol with associated type, a type which must inherit from protocol A. If I try to declare it as :

protocol AnotherProtocol{
    associatedtype Type : A
    weak var type : Type?{get set}
}

it compiles without errors but when trying to adopt AnotherProtocol in the following scenario:

class SomeClass : AnotherProtocol{

    typealias Type = B
    weak var type : Type?
}

compilation fails with error claiming that SomeClass does not conform to AnotherProtocol. If I understood this correctly it means that B does not adopt A while Im trying to declare and asking you on how to declare an associated type which inherits from protocol A?

I made the above assumption based on fact that the following scenario compiles just fine

class SomeDummyClass : B{

}

class SomeClass : AnotherProtocol{

    typealias Type = SomeDummyClass
    weak var type : Type?
}
1

There are 1 answers

5
Hamish On

This is pretty interesting. It appears that once you constrain the type of an associatedtype in a given protocol, you need to provide a concrete type in the implementation of that protocol (instead of another protocol type) – which is why your second example worked.

If you remove the A constraint on the associated type, your first example will work (minus the error about not being able to use weak on a non-class type, but that doesn’t seem to be related).

That all being said, I can't seem to find any documentation in order to corroborate this. If anyone can find something to back this up with (or completely dispute it), I’d love to know!

To get your current code working, you can use generics. This will actually kill two birds with one stone, as both your code will now compile and you'll benefit from the increased type safety that generics bring (by inferring the type you pass to them).

For example:

protocol A : class {}
protocol B : A {}

protocol AnotherProtocol{
    associatedtype Type : A
    weak var type : Type? {get set}
}

class SomeClass<T:B> : AnotherProtocol {
    typealias Type = T
    weak var type : Type?
}

Edit: It appears the above solution won't work in your particular case, as you want to avoid using concrete types. I'll leave it here in case it's useful for anyone else.


In your specific case, you may be able to use a type erasure in order to create a pseudo concrete type for your B protocol. Rob Napier has a great article about type erasures.

It's a bit of a weird solution in this case (as type erasures are normally used to wrap protocols with associatedtypes), and it's also definitely less preferred than the above solution, as you have to re-implement a 'proxy' method for each method in your A & B protocols – but it should work for you.

For example:

protocol A:class {
    func doSomethingInA() -> String
}

protocol B : A {
    func doSomethingInB(foo:Int)
    func doSomethingElseInB(foo:Int)->Int
}

// a pseudo concrete type to wrap a class that conforms to B,
// by storing the methods that it implements.
class AnyB:B {

    // proxy method storage
    private let _doSomethingInA:(Void)->String
    private let _doSomethingInB:(Int)->Void
    private let _doSomethingElseInB:(Int)->Int

    // initialise proxy methods
    init<Base:B>(_ base:Base) {
        _doSomethingInA = base.doSomethingInA
        _doSomethingInB = base.doSomethingInB
        _doSomethingElseInB = base.doSomethingElseInB
    }

    // implement the proxy methods
    func doSomethingInA() -> String {return _doSomethingInA()}
    func doSomethingInB(foo: Int) {_doSomethingInB(foo)}
    func doSomethingElseInB(foo: Int) -> Int {return _doSomethingElseInB(foo)}
}

protocol AnotherProtocol{
    associatedtype Type:A
    weak var type : Type? {get set}
}

class SomeClass : AnotherProtocol {
    typealias Type = AnyB
    weak var type : Type?
}

class AType:B {
    // implement the methods here..
}
class AnotherType:B {
    // implement the methods here..
}

// your SomeClass instance
let c = SomeClass()

// set it to an AType instance
c.type = AnyB(AType())

// set it to an AnotherType instance
c.type = AnyB(AnotherType())

// call your methods like normal
c.type?.doSomethingInA()
c.type?.doSomethingInB(5)
c.type?.doSomethingElseInB(4)

You can now use the AnyB type in place of using the B protocol type, without making it any more type restrictive.