Protocol extension dispatch anomalies

I have put together a (slightly complicated) protocol hierarchy whose dispatch behavior is still puzzling me so I decided to share it in the hope that someone could explain the rationale behind it.


Here is the code (sorry for the obfuscation and the length, this is the minimal set that still reproduces the issue):


protocol Base: Foo {
    var baseProp: Int { get }
}

protocol Foo {
    func doFoo()
}

protocol SpecialFoo: Foo {}

extension SpecialFoo where Self : Base, Self : Bar {
    func doFoo() {
        _ = self.baseProp
        self.doBar()
    }
}

protocol Bar {
    func doBar()
}

protocol Qux {
    var quxProp: Int { get }
}

extension Bar where Self : Base, Self : Qux {
    func doBar() {
        _ = self.baseProp
        _ = self.quxProp
        print("default impl for Qux")
    }
}

extension Bar {
    func doBar() {
        print("default impl")
    }
}

class Generic<T>: Base, SpecialFoo, Bar {
    var baseProp: Int { get { return 0 } }
}

class Derived: Generic<Int>, Qux {
    var quxProp: Int { get { return 0 } }
}


let d = Derived()
d.doFoo()


If you paste this in a playground, you'll see that it will print "default impl", whereas my expectation was "default impl for Qux". My reasoning was that since "d" is a Derived, when I call doFoo() the SpecialFoo's implementation is hit (so far this is the case), which in turn would call the doBar() implemented in the more specialized extension (given that Derived in fact implements Qux). But it's not what happens, and I can't figure out why.


Could anyone give me some insights?

I dunno, but it looks like it might be the same thing as this:


https://forums.developer.apple.com/thread/11126


Specifically, there's something like inheritance that's supposed to be going on here, but it isn't like any kind of inheritance that we know elsewhere in the language. It may be a bug, or it may be a limitation.


My example was reported as bug #21885544, if you're inclined to report yours as a duplicate.

There's some extra code here that's just muddying the waters. Would you agree that this simplified version summarizes the behavior you're asking about?


protocol Foo { func doFoo() }
protocol Bar { func doBar() }

extension Foo where Self : Bar {
    func doFoo() {
        self.doBar()
    }
}

extension Bar {
    func doBar() {
        print("Default doBar")
    }
}

protocol Qux {}

extension Bar where Self : Qux {
    func doBar() {
        print("Default doBar for Quxes")
    }
}

class BaseClass: Foo, Bar {}
class DerivedClass: BaseClass, Qux {}

let d = DerivedClass()

d.doFoo() // Calls default doBar instead of Qux-specific doBar


This code behaves as it does because protocol extensions are compile-time constructs. They do not create implementation hierarchies, and the specific implementation that is called depends on the amount of type information the compiler has available at compile time. Within "extension Foo where Self : Bar", all that is known about self is that it's a Bar. So method calls are dispatched as for a Bar.


Generic functions work pretty much the same way. If you understand the behavior of the following code, then you understand protocol extensions:


protocol Foo {}
protocol Qux: Foo {}

func callDoFoo<T: Foo>(x: T) {
    doFoo(x)
}

func doFoo<T: Foo>(x: T) {
    print("Called default doFoo")
}

func doFoo<T: Qux>(x: T) {
    print("Called doFoo for Quxes")
}

class Base: Qux {}

let obj = Base()

callDoFoo(obj)  // Calls default doFoo
doFoo(obj)      // Calls doFoo for Quxes

I see your point but something is still missing here. If I understand it correctly, the self.doBar() call is not dispatched dynamically but based on the static type information. So if the compiler knew that the current extension applies to Quxes, it would call the Qux-specific doBar() in the end, right? I modified your example accordingly:


protocol Foo { func doFoo() }
protocol Bar { func doBar() }

extension Foo { // required to make BaseClass compile
    func doFoo() {}
}

extension Foo where Self : Bar, Self : Qux { // note the added Self : Qux constraint
    func doFoo() {
        self.doBar()
    }
}

extension Bar {
    func doBar() {
        print("Default doBar")
    }
}

protocol Qux {}

extension Bar where Self : Qux {
    func doBar() {
        print("Default doBar for Quxes")
    }
}

class BaseClass: Foo, Bar {}
class DerivedClass: BaseClass, Qux {}

let d = DerivedClass()

d.doFoo() // Still calls default doBar instead of Qux-specific doBar


This code still invokes the more general doBar(), although the compiler could now deduce that there is a more specific implementation.

>> This code behaves as it does because protocol extensions are compile-time constructs.


The problem with this is that it doesn't seem to be viable, as a compiler behavior:


1. In the case of a class type conforming to the protocol(s) involved, the existing semantics of inheritance say that it doesn't matter (to instances of a subclass) whether a method is implemented in the subclass or a base class (when it's only implemented in one place, I mean, I'm not talking about overrides). If that weren't true, then it wouldn't be safe to factor behavior out of subclasses into base classes.


2. In your 'extension Foo' example, the issue isn't whether the doFoo method is a compile-time construct, but the fact that 'self' already means something in terms of method lookup — it means to start with the class of the instance that's executing the code**. Unfortunately, in the current Swift, it means something else, apparently to start with a class chosen at compile time. Whatever that is, it isn't what 'self' means.



** That might not be what happens when the method is final, but the protocol extension method is not final. When it's also listed in the protocol, it's documented as being "replacable" by a method from a conforming class. That replacement must be dynamically dispatched at run-time, or we've ended up with something that's not inheritance, but not not inheritance.

This is an excellent question, and I don't have an explanation for this behavior. Perhaps one of the language devs could clarify what's going on in this case.

I'm not sure that I understand the point you're making here, so please correct me if I'm misinterpreting. (And note also, I'm going to just ignore potential @objc complications.)


I think you're saying that given the following code context,


protocol P {
    func p()
}

extension P {
    func p() { print("Called default p implementation") }
}

class C: P {}

func someFunction(obj: C) {
    obj.p()
}


the dispatch of obj.p() on line 12 must take account of the fact that obj may in fact be a subclass of C with its own p() implementation, and that that subclass's implementation of p() should be called in preference to the default p(). Otherwise, OO method inheritance might be considered broken.


However, that is not in fact the behavior of the current implementation:


// This is a continuation of the previous code...

class D: C {
    func p() { print("Called D's p implementation") }
}

someFunction(D()) // Calls the default p implementation


I agree that this is weird. I'm just pointing out that this is how it works.


Mechanically, I think the compiler just says, "OK, I have a C. Does a C actually implement p()? Nope! Good thing I have this default implementation I can call instead."


Ideally, the default implementation of p() would get a ref in C's vtable based on C's declared compliance with P, and subclasses of C would have to use the override keyword to declare their own implementations. That would guarantee that things work as Quincey expects, but I imagine it would also impose a variety of restrictions on the use of protocol extensions.

>> However, that is not in fact the behavior of the current implementation:

>> […]

>> I agree that this is weird. I'm just pointing out that this is how it works.


I'm saying it's not just weird, it's wrong. The really weird thing is that it's wrong in 3 completely separate ways:


1. P's p and D's p are actually unrelated methods of the same name. Note that there's no error message stating that D's p needs an override keyword, or that the two p's conflict (which they do).


2. Class D conforms to P, and it's documented that "If a conforming type provides its own implementation of a required method or property, that implementation will be used instead of the one provided by the extension". It isn't used.


3. If you put a definition of p in class C, then that implementation *is* used instead of the one provided by the extension. By what mechanism? It's not regular inheritance (again, there's no need of an 'override' keyword, which real inheritance would require, and it works for structs which can't inherit). Instead, it's some kind of method replacement, a brand-new language feature which breaks (see point #2) when combined with regular inheritance.

I think that's a bug. It may have been resolved in beta 4; if not please file it at bugreport.apple.com!

Wow, that's all kinds of bad... strikes me as the opposite of "Safe"


If this protocol extension stuff is going to work in the long term (and I hope it does, because it solves a lot of problems for me), it's got to be a lot less wonky than that or there's no safe way to use it 😟

I just tried it, and the problem (that is, the subclass method not replacing the protocol extension default method) is not fixed in beta 4.

Still happening in beta 4 for me, too. Here's a simplified test case that shows it more clearly to be a bug:


protocol Foo { func doFoo() }
protocol Qux {}

extension Foo {
    func doFoo() { print("Called default doFoo") }
}

extension Foo where Self: Qux {
    func doFoo() { print("Called Qux-specific doFoo") }
}

class BaseClass: Foo {}
class DerivedClass: BaseClass, Qux {}

class NativeFooQuxClass: Foo, Qux {}

let n = NativeFooQuxClass()
let d = DerivedClass()

n.doFoo() // Calls Qux-specific doFoo - correct
d.doFoo() // Calls Qux-specific doFoo - correct

let np: protocol<Foo, Qux> = n
let dp: protocol<Foo, Qux> = d

np.doFoo() // Calls Qux-specific doFoo - correct
dp.doFoo() // Calls default doFoo - incorrect


Apparently, the internal structure of the actual type has some bearing on protocol extension dispatch.


Filed rdar://21967880.

Protocol extension dispatch anomalies
 
 
Q