Protocol Extensions, Defaults, and “Overriding”

Table of Content

Protocol Extensions, Defaults, and "Overriding"

Basic Protocol Conformance

So I’ve previously thought of protocol extensions as having the ability to provide "default implementations" for a protocol function, but there’s a lot of nuance I didn’t understand as well until the following experiment.

In this, we will be defining some requirements of a protocol, implementing some in the extension, some only in the conforming type, and some in both places to see that affects which code gets called where.

Let’s start with a simple example:

import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {
    func justInProtocol()
}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {}

struct MyExampleImplementation: MyExampleProtocol {

    /// This function is a requirement of the `MyExampleProtocol` protocol, so all types conforming will need to provide some sort of an implementation for this function. The code will *always* be the following, regardless if the instance is cast as a `MyExampleProtocol` or `MyExampleImplementation`.
    func justInProtocol() {
        print("This is from protocol - required for struct to implement.")
    }

}

let test = MyExampleImplementation()
test.justInProtocol()
// prints out:
// This is from protocol - required for struct to implement.

(test as MyExampleProtocol).justInProtocol()
// prints out:
// This is from protocol - required for struct to implement.

As we can see, this is fairly simple and straightforward. It’s also fairly easy to say that it works as expected.

Using a Default Implementation

Let’s implement a default implementation for a protocol function and use the default.

import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {
    func inBothProtocolAndExtensionHasAndUsesDefault()
}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {
    /**
    This is the extension providing a default implemenation of `inBothProtocolAndExtensionHasAndUsesDefault`. Conforming types will not need to implement it, since the extension provides an implementation. In this example, the conforming type does NOT provide its own implementation, so the default one is used.
    */
    func inBothProtocolAndExtensionHasAndUsesDefault() {
        print("This is 'inBothProtocolAndExtensionHasAndUsesDefault' from the extension (think of as a 'default implementation')!")
    }
}

struct MyExampleImplementation: MyExampleProtocol {}

let test = MyExampleImplementation()
test.inBothProtocolAndExtensionHasAndUsesDefault()
// prints out:
// This is 'inBothProtocolAndExtensionHasAndUsesDefault' from the extension (think of as a 'default implementation')!

(test as MyExampleProtocol).inBothProtocolAndExtensionHasAndUsesDefault()
// prints out:
// This is 'inBothProtocolAndExtensionHasAndUsesDefault' from the extension (think of as a 'default implementation')!

Again, this is fairly simple and straightforward and continues to work as expected.

"Override" The Default Implementation

This time let’s create a required protocol function, provide a default, but despite having a default, we want our own implementation:

import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {
    func inBothProtocolAndExtensionHasButNotUseDefault()
}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {

    /**
    This is the extension providing a default implemenation of `inBothProtocolAndExtensionHasButNotUseDefault`. Conforming types will not need to implement it, since the extension provides an implementation. In this example, the conforming type provides its own implementation, so the default one is NOT used.
    */
    func inBothProtocolAndExtensionHasButNotUseDefault() {
        print("This is 'inBothProtocolAndExtensionHasButNotUseDefault' from the extension (think of as a 'default implementation')!")
    }
}

struct MyExampleImplementation: MyExampleProtocol {
    /// This function is a requirement of the `MyExampleProtocol` protocol, but a default implementation is provided via the protocol extension. The code will *always* be the following, regardless if the instance is cast as a `MyExampleProtocol` or `MyExampleImplementation`.
    func inBothProtocolAndExtensionHasButNotUseDefault() {
        print("This is 'inBothProtocolAndExtensionHasButNotUseDefault' from the STRUCT!")
    }
}

let test = MyExampleImplementation()
test.inBothProtocolAndExtensionHasButNotUseDefault()
// prints out:
// This is 'inBothProtocolAndExtensionHasButNotUseDefault' from the STRUCT!

(test as MyExampleProtocol).inBothProtocolAndExtensionHasButNotUseDefault()
// prints out:
// This is 'inBothProtocolAndExtensionHasButNotUseDefault' from the STRUCT!

This is another fairly tame example, doing precisely what we would expect.

Extension only

Okay, so what if we only implement a function in the extension?


import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {
    /// This is not a 'requirement' of the protocol, but all conforming types get this function for free. This example is provided to see what happens if you don't provide your own implementations.
    func justInTheExtension() {
        print("This is 'justInTheExtension' from the extension!")
    }
}

struct MyExampleImplementation: MyExampleProtocol {}

let test = MyExampleImplementation()
test.justInTheExtension()
// prints out:
// This is 'justInTheExtension' from the extension!

(test as MyExampleProtocol).justInTheExtension()
// prints out:
// This is 'justInTheExtension' from the extension!

I’d personally argue this is consistent with what I’d expect. What about you?

Let’s keep digging…

Extension and Struct Implementations

This is where things might start to get hairy. From this point forward, the protocol requires nothing by itself.

import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {

    /// This is not a 'requirement' of the protocol, but all conforming types get this function for free. However, if a conforming type provides its own implementation, there might be some gotchas you wouldn't expect. See examples below.
    func inTheExtensionAndStruct() {
        print("This is 'inTheExtensionAndStruct' from the extension!")
    }
}

struct MyExampleImplementation: MyExampleProtocol {
    /// This function is NOT a requirement of the `MyExampleProtocol` protocol! But with the protocol extension providing an implemenation for it, all conforming types get it for free. In this case, we are providing our own implemenation, but it will only be utilized when your type is identified as itself and not via the conforming protocol. See examples below.
    func inTheExtensionAndStruct() {
        print("This is 'inTheExtensionAndStruct' from the STRUCT!")
    }
}

let test = MyExampleImplementation()
test.inTheExtensionAndStruct()
// prints out:
// This is 'inTheExtensionAndStruct' from the STRUCT!

(test as MyExampleProtocol).inTheExtensionAndStruct()
// prints out:
// This is 'inTheExtensionAndStruct' from the extension!

Is that what you expected?! Keep in mind this is the exact same object underneath, but it behaves differently depending on how it’s cast.

Fake ‘Super’ Call

Using class inheritence in Swift, Objective C, and honestly, probably most other OOP languages out there, you have the ability to override superclass methods with your own implementation. Doing so requires you to follow one of two scenarios: Either call the superclass method or don’t. Sometimes you have a choice, but usually there’s a right and wrong way to do it.

Using protocols, by default there’s no "super type" as the protocol is an abstract concept (think ‘interface’), whereas a class is concrete. The conforming type is the implementation of said abstract interface, therefore there’s no "super". However, while being super duper cool, protocol extensions kind of muddy this concept. What if you want to call the default implementation AND add some custom functionality on top?

I’m going to warn you right here, this is likely not considered good practice. But at the very least it’s a fun experiment!

Pay special attention to the test examples at the bottom.

import Foundation

/// Defines what conforming types will need to implement to be able to 'conform' to `MyExampleProtocol`.
protocol MyExampleProtocol {}

/// The extension can provide some default functionality that all `MyExampleProtocol` conforming types will automatically be able to perform, just by conforming.
extension MyExampleProtocol {
    /// This is not a 'requirement' of the protocol, but all conforming types get this function for free. This example is provided to see what happens in an attempt to fake a 'super call' like you can do with class inheritence. This is not where the voodoo happens, check out the conforming type below.
    func inTheExtensionAndStructFakeSuperCall() {
        print("This is 'inTheExtensionAndStructFakeSuperCall' from the extension!")
    }
}

struct MyExampleImplementation: MyExampleProtocol {
    /**
    Just look at the printout below
    */
    func inTheExtensionAndStructFakeSuperCall() {
        // fake 'super' call:
        (self as MyExampleProtocol).inTheExtensionAndStructFakeSuperCall()
        print("And this is a continuation of the struct version of the fake super call.")
    }
}

So let’s test it!

let test = MyExampleImplementation()
(test as MyExampleProtocol).inTheExtensionAndStructFakeSuperCall()
// prints out:
// This is 'inTheExtensionAndStructFakeSuperCall' from the extension!

Okay, so it’s performing as expected so far. When called generically as the abstract protocol, it just uses the protocol’s extension implementation.

But what happens when we call it as the concrete type?

test.inTheExtensionAndStructFakeSuperCall()
// prints out:
// This is 'inTheExtensionAndStructFakeSuperCall' from the extension!
// But this is a continuation of the struct version of the fake super call.

How neat is that?!

There’s always a catch

While that’s cool and all, here’s why I think it’s a bad idea. Consider the following scenario:

func doAThing(with this: MyExampleProtocol) {
    this.inTheExtensionAndStructFakeSuperCall()
}

doAThing(with: test)

What do you expect as output?

Don’t worry, it’s not a trick question. It performs consistently with our earlier tests; it will only print out This is 'inTheExtensionAndStructFakeSuperCall' from the extension! despite the fact that MyExampleImplementation is the underlying object and has its own function called the exact same thing. (this also applies to the previous inTheExtensionAndStruct example)

But when you’re in the middle of writing all your protocols, extensions, and conforming your types, you might be expecting it to run whatever your code is as opposed to what you thought you were overriding. (and if it’s not apparent, the simple fix is to add the function to the protocol requirements, assuming you weren’t relying on this behavior elsewhere).

Ultimately, I think the most important takeaway is that you really need to pay attention to the interfaces you provide in protocols when used in combination with protocol extensions for the purpose of default implementations.

As an additional thought, I think the Swift compiler should perform the following: When a protocol extension defines a new function (NOT in the protocol requirement) and the conforming type ALSO provides an identical implementation name, a warning should be displayed informing that call sites referring to the type via its protocol will not execute the code currently in front of you. As a concern that this might be intentional in some cases, to silence the warning, I think a keyword should be applied to the function to tell the compiler that this was deliberate, but I have no idea what the keyword should be.

Leave a Reply

Your email address will not be published. Required fields are marked *