Developear
All Apps   Steps+   Twitter   Contact   RSS  



This problem has been written about and discussed briefly in a few forums, so I will only go over it as briefly as I can here; and then I will jump into a way the Swift language and/or compiler could potentially mitigate this issue. Because of this, I am gonna use a pretty simple and relatively contrived example. Let's get into it.

The Problem

Default implementations in Swift protocols are a fundamental part of how the Swift Standard Library works, as well as many other systems in our applications. They can be useful for adding behavior to existing types, or providing default behavior for select methods.

However, it can lead to surprising, and potentially dangerous situations. Take this simple protocol as an example:


protocol TestProtocol {
    func test(string: String)
}

and this default implementation...


extension TestProtocol {
    func test(string: String) {
        print("\(string)")
    }
}

This is a pretty simple default implementation, that just prints the input. So lets add this behavior to a type and call the test method:


class TestClass: TestProtocol {

}

let testClass = TestClass()

testClass.test(string: "hello")

and as you would expect "hello" is printed. However, at some point, we implement (read: override) func test(string: String) in testClass that simply adds the string to an array for future use (maybe we are building some sort of analytics logger, or something). Our implementation now looks like this:


class TestClass: TestProtocol {
    private var strings = [String]()
    
    func test(string: String) {
        strings.append(string)
        print("\(string)")
    }
}

Perfect! Now we can look at everything ever passed into here later! However, what happens if this functions signature is changed in TestClass? The compiler doesn't complain at all! Our code now looks something like this, without compiler error - but introducing a runtime bug into our program.


class TestClass: TestProtocol {
    private var strings = [String]()
    
    // New parameter: `secondString`
    func test(string: String, secondString: String) { // No error or warning!
        // Use secondString for something
        strings.append(string)
        print("\(string)")
    }
}

let testClass = TestClass()

testClass.test(string: "hello") // Calls default implementation now

Our call site now calls the default implementation, despite that not being the intention of the original author of the TestClass class.

This example is pretty simple, and the consequences are low; however, once you start having multiple implementors, lots of indirection via even the simplest of Protocol Oriented Programming, or a run of Swift upgrade migration that automatically changes method signatures - you can introduce a lot of pretty nasty bugs because of this (this has happened to me at Tumblr many times already). Because there is no compiler error or warning - it can be nearly impossible to tell you have broken anything at all - and thus subtle bugs slipping into production code.

The Solution(?)

If the compiler acted similar to how it acts when overriding methods from a superclass (even though the dispatch behavior is different for protocol extensions) - this bug would not exist.

For example, if I re-architected this mini system to use a base class it would look something like this (note the override keyword in the subclass's implementation):


class TestBaseClass {
    func test(string: String) {
        print("\(string)")
    }
}

class TestClass: TestBaseClass {
    private var strings = [String]()
    override func test(string: String) {
        strings.append(string)
        print("\(string)") // or call super
    }
}

let testClass = TestClass()

testClass.test(string: "hello")

Now, if I ended up changing that subclass's function definition of override func test(string: String) in anyway - we get a compiler error, letting us know that the Method does not override any method from its superclass.

My proposed solution is to have the Swift compiler require the override keyword in the same way it does for overriding methods on a superclass. I'd vastly rather always use Protocol Oriented Programming because of it's many benefits, and not have to make subclasses - but the current state of default implementations in protocol extensions gives me pause.

I am sure there are other viable solutions to this; for example I have seen proposals for abstract base class and methods language construct.

Let me know what you think on Twitter.