Protocols with requirements that only other protocols can implement

here’s a useful protocol:

protocol DatabaseCollection
{
    ...
}

it has lots of requirements, but the only thing we care about right now is that it has an extension method called setup(with:) that does exactly what it sounds like:

extension DatabaseCollection
{
    func setup(with session:Mongo.Session) async throws
    {
        ...
    }
}

of course, one protocol DatabaseCollection couldn’t possibly cover all possible use-cases, we might want to have a derived protocol DatabaseCollectionCapped¹ like:

protocol DatabaseCollectionCapped:DatabaseCollection
{
    var capacity:Int { get }
}

naturally, you need to do some extra things to setup a DatabaseCollectionCapped, so it also has an extension method called setup(with:) that shadows the one in the superprotocol.

extension DatabaseCollectionCapped
{
    func setup(with session:Mongo.Session) async throws
    {
        func _super(self:some DatabaseCollection) async throws
        {
            try await self.setup(with: session)
        }

        try await _super(self: self)
        ...
    }
}

that won’t work too well, because every type that conforms to DatabaseCollectionCapped will get two members called setup(with:) and only one of them is correct to use. and sometimes we want to intentionally call the wrong one (like in _super(self:)), but usually we end up unintentionally calling the wrong one.

you could go the opposite direction and make setup(with:) a requirement of DatabaseCollection.

protocol DatabaseCollection
{
    func setup(with session:Mongo.Session) async throws
    ...
}

but then the derived protocol loses the ability to call the base protocol’s setup(with:) implementation. plus, you would never want to implement it directly, you would only ever want to inherit it from a protocol.

what is the best way to achieve a class-like hierarchy of protocol implementations, without actually resorting to class inheritance?


[1] in six months, the name will become less awkward!

Is that the same as:

          try await (self as DatabaseCollection).setup(with: session)

Please give an example. The only way I find calling the wrong "setup" is by typing the thing as DatabaseCollection, if it is typed as its own type (e.g. a struct type conforming to DatabaseCollectionCapped) then the right "setup" is getting called.

for the past few releases of swift, yes it is. that is just me cargo-culting existentials into generics :slight_smile:

that is the main example, any use of a DatabaseCollectionCapped-conforming type in a generic context will call the wrong implementation. it is a well-known footgun.

Try this device:

protocol Base {
    func setup()
}
extension Base {
    func setup() {
        baseSetup()
    }
    func baseSetup() {
        print("base setup")
    }
}

protocol Derived: Base {}
extension Derived {
    func derivedSetup() {
        baseSetup()
        print("derived additional setup")
    }
    func setup() {
        derivedSetup()
    }
}

struct Value: Derived {}

Somewhat mouthy but seems to be working alright:

func test<T: Base>(_ v: T) {
    v.setup()
}

let value = Value()
value.setup()               // base setup + derived additional setup
(value as Derived).setup()  // ditto
(value as Base).setup()     // ditto
test(value)                 // ditto
2 Likes

yeah, that’s pretty much what i am using right now. i’m just not a fan of baseSetup showing up as a member of every type that conforms to these protocols.

the real (and probably inevitable) solution is likely to move the protocols to their own module and make the helpers internal. but that would necessitate dousing the entire CRUD API in @inlinable, since unspecialized Codable performance is just awful. sigh…