[Pitch] Improve protocol inheritance behaviour

Hi all,

This was a pitch I started at the beginning of last year but due to time constraints and the horrible mailing list workflow I never got around to pushing it further and replying to any feedback. I need to update it a little bit to bring it up to date, but the core pitch is still trying to solve an existing issue.

The pitch can be found here: Improve protocol inheritance behaviour · GitHub

At the core of the pitch is the problem where this scenario happens:

// Define the protocol
protocol MyProtocol {

    func doTheFirstThing()
    func doTheSecondThing()
    func doTheThirdThing()
}

// Add an extension to implement some default behaviours
extension MyProtocol {

    func doTheFirstThing() {
        print("The 'DEFAULT' 1st method")
    }

    func doTheSecondThing() {
        print("The 'DEFAULT' 2nd method")
    }

    func doTheThirdThing() {
        print("The 'DEFAULT' 3rd method")
    }
}

// Have a class conform to the protocol
class MyClass: MyProtocol {

    /// Implementing a version of 'doTheFirstThing()'
    func doTheFirstThing() {
        print("'MyClass' implementation of the 1st method")
    }

    func doTheThirdThing() {
        print("'MyClass' implementation of the 3rd method")
    }

    func callMethods() {

        self.doTheFirstThing()
        self.doTheSecondThing()
        self.doTheThirdThing()
    }
}

// Subclass the class that conforms to the protocol and implement one of the methods it defines
class MySubclass: MyClass {

    /// Implementing a version of 'doTheSecondThing()'
    func doTheSecondThing() {
        print("'MySubclass' implementation of the 2nd method")
    }

    override func doTheThirdThing() {
        print("'MySubclass' overridden implementation of the 3rd method")
    }
}

// Calling the methods individually from outside the subclass
let subclass = MySubclass()
subclass.doTheFirstThing()
subclass.doTheSecondThing()
subclass.doTheThirdThing()

print("---------------")

// Calling the methods from a method within the superclass
MySubclass().callMethods()

Calling the methods from inside and outside of the class behave in different ways and yield the following console output:

'MyClass' implementation of the 1st method
'MySubclass' implementation of the 2nd method
'MySubclass' overridden implementation of the 3rd method
---------------
'MyClass' implementation of the 1st method
The 'DEFAULT' 2nd method
'MySubclass' overridden implementation of the 3rd method

This is obviously not the desired behaviour and something that needs to be addressed, hence my pitch.

I dont pretend to have the best solution to this problem yet which is why I want to start a discussion around it. Please let me know your thoughts.

3 Likes

This is the intended behavior. Shockingly.

There’s a classic blog post about it: The Ghost of Swift Bugs Future

There was a long, long discussion of this back in the early days of the list, during which @Chris_Lattner3 suggested that better warnings about shadowing of statically dispatched names was the answer. I personally would love to see that.

I remember that @beccadax put a lot of work into trying to make this behavior more customizable, declarative, and intentional, and found that it got really messy. IIRC @Erica_Sadun had a thought or three on the matter as well. I wonder if anything came of all that?

Interesting, I wasn't aware of the previous discussions, only that the original bug ([SR-103] Protocol Extension: function's implementation cannot be overridden by a subclass · Issue #42725 · apple/swift · GitHub) on JIRA was still open and that I was the last person to comment on it.

I can see why it's the intended behaviour but at the same time it could lead some very strange bugs for anyone who came across it and didn't know about the quirk. The warnings suggestion would at east inform the user of the issue and it could suggest to implement the offending method in the subclass to call through to the superclass, but it's a bit of a 'meh' solution compared to changing the behaviour to be predictable.

I will try and have a look for and read though the discussions that you mentioned, maybe there is something I'm missing.

This behavior has bitten me twice in my limited time with Swift, where I was interacting with another module which simulated Objective-C optional protocol methods by using default implementations. If I didn't have access to the source code, it would've been nigh impossible to figure out why my implementation wasn't called.

I consider this one of the worst parts of Swift, and I hope this can be addressed. The expected behavior would always be to call the implementations in precedence of subclass -> base class -> default implementation, which I suppose would necessitate dynamic dispatch. Perhaps if we required protocol implementations to specify their conformance (similar to declaring override), a really intelligent compiler could parse out the correct method statically.

I think you are describing a different thing. That blog post talks about protocol extension methods which are not requirements of the protocol.

In fact, that blog post is flat-out *wrong* when it comes to the scenario Dale is referring to. The post says:

However in Dale’s situation the first two bullets are true, but the runtime type’s implementation is *not* called. Here is a minimal example:

protocol Fooable { func foo() }
extension Fooable { func foo() { print("Default foo") } }
class A: Fooable {}
class B: A { func foo() { print("B-specific foo") } }
let b: Fooable = B()
b.foo()     // "Default foo"
4 Likes

Yes, the problem outlined is a very different thing from what @Paul_Cantrell is discussing.

It’s unfortunate because the title of this thread is misleading, whereas the bug, SR-103, is very clear. I would strongly suggest closing out this thread and re-pitching your idea with a rewritten post. Otherwise, this will get quickly derailed and go nowhere.

2 Likes

Ah, I see; the minimal example helps. That seems like an unequivocal bug, not even a language evolution discussion.