There's a few things here that aren't quite right:
This isn't necessarily a problem, per se, just a place where Swift and Objective-C differ. Non-public methods in one module can never get invoked from another module or and are never chosen to satisfy requirements for a conformance in another module.
This is a difference between methods in a protocol declaration and methods in a protocol extension, but I don't think it's relevant here: in this case the Mixin protocol does declare fin() as a requirement and thus a dynamic dispatch entry point.
I guess both of these points would be relevant if Swift did use name-based dispatch (like Objective-C does), but it doesn't (when a method's not exposed to Objective-C) because of the problems that @itaiferber brought up. If we're all on the same page about that…
…let's back up. How does protocol inheritance work today? It's essentially a shorthand for an additional requirement on the conforming type. That is, these two are equivalent:
protocol Sub: Base {}
protocol Sub where Self: Base {}
struct Impl: Sub {}
In both cases, anything that's generic over Sub can make use of the value also being a Base, and so there must be a way to take a Sub-constrained value and use it as a Base-constrained value. The implementation of this is for the run-time representation of the Impl: Sub conformance to include a reference to the Impl: Base conformance.
Okay, so how would we extend this to "mixin conformances"? Well, it gets tricky. As you note, we could have a rule that whenever you have an Opaque conformance, you can use that to build a Mixin conformance using the default implementations provided. That's totally implementable! However, it gets weird in cases like this:
struct Concrete: Opaque, Mixin {
typealias T = String
func fin() -> Int { return 20 }
}
func useOpaque<T: Opaque>(_ value: T) {
value.fin()
}
useOpaque(Concrete())
In a language like C++, we'd get a different version of useOpaque for each concrete type that gets used, but Swift doesn't work like that. All useOpaque knows is:
- the run-time concrete type of T, which is Concrete
- how that type conforms to Opaque
So if it uses the default implementation of Opaque: Mixin (where fin() returns 42), it'll have different behavior from Concrete: Mixin (where fin() returns 20). Hm. But useOpaque doesn't have access to Concrete: Mixin—at least, not without doing dynamic lookup. And dynamic lookup has its own problems (mostly the "what if two modules independently implement the same protocol" problem).
But let's say we go with dynamic lookup. That still doesn't solve all the problems: what happens in this case?
// Module Base
protocol A {}
protocol B {}
struct Concrete: A, B {}
// Module Mixin
extension A: Mixin {
func fin() -> Int { return 1 }
}
extension B: Mixin {
func fin() -> Int { return 2 }
}
Now we have the same problem: which implementation should we use when trying to form Concrete: Mixin? Neither one is obviously better than the other. (This is the same reason why you're not allowed to have a type conform to a protocol with two different sets of conditions—there may not be one that's better than the other.)
None of these problems are insurmountable in the abstract; it would be possible to define behavior in all these cases. But what would be hard is doing it in a way that doesn't compromise performance and doesn't make the language (too much) more complicated than it already is. So the recommended approach today is to build adapters instead:
struct OpaqueMixinAdapter<RawValue: Opaque>: Mixin {
var rawValue: RawValue
init(_ rawValue: RawValue) {
self.rawValue = rawValue
}
func fin() -> Int { return 42 }
}
It does mean that anyone who wants to use an Opaque as a Mixin has to hop through your adapter type, but at least the behavior is very clear.