Default implementations for protocols and extending protocols

Motivation

My idea is, that Swift should be more consistent on using the extension keyword on a protocol.

Background

The Swift Documentation says:

"Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you do not have access to the original source code"

In case of classes or structs I can extend by adding new methods. I need to define them in the extension and then my class or struct gets a new method.

But Swift goes further and also allows the extension keyword on a protocol. The documentation says:

"In Swift, you can even extend a protocol to provide implementations of its requirements or add additional functionality that conforming types can take advantage of." More is explained here.

My thoughts on this

I think, that this is not the right usage of the extension keyword. With this way, you don't extend the protocol, you implement methods. This can lead to the problem, that you may make mistakes, if you will overwrite the default implementations (look here why).

Also, it's not possible to really extend a protocol like this:

protocol A {
    func myFuncA()
}

extension A {
   func myFuncB()   
   // this doesn't work, because Swift now expects the code for the implemenation
   // -> error: Expected '{' in body of function declaration
}

And I think, that this should be the right way of extending a protocol. Extending a class means adding more methods, extending a protocol should mean adding more methods, that the class/struct should implement.

Extending a protocol like this could be necessary, if you really want to extend the protocol (or if you need to, because you don't have access to the original code). At the moment, you just can create a new protocol like this:

protocol B: A {
    func myFuncB()
}

class MyClass: B { ... }

Of course, that would work perfectly right now, but in my opinion it's inconsistent (you even see it by creating this empty class: Xcode tells you, that it doesn't conform to protocol A and it doesn't conform to protocol B... so it's not an extension, it's just combining two protocols).

My idea for a change

I would suggest to introduce a new keyword for protocol implementations and leave the extension keyword for defining more required methods, that need to be implemented. As example:

Without extension:

protocol A {
    func myFuncA()
}

class MyClass: A {
    // the class just needs to implement the myFuncA()
    func myFuncA() {
        ...
    }
}

With extension:

protocol A {
   func myFuncA()
}

extension A {
    func myFuncB()
}

class MyClass: A {
    func myFuncA() {
        ...
    }
    // now the class also needs to implement the extended methods
    func myFuncB() { 
        ...
    }
}

Now an example for making a default implementation:

protocol A {
   func myFuncA() 
}

implementation A {
    func myFuncA() {
        print("I'm a default implementation")
    }
}

class MyClass: A {
   // now the class can be empty
}

Solving the side problem: Overwriting a default implementation

This suggestions leads to the same side problem that already exists right now and what was the reason for my original posting: If you want to overwrite the default implemenation and if you make a typo at the parameter names while doing this, the compiler wouldn't complain about this. For example:

protocol A {
   func myFuncA(parameter1: Int, parameter2: Int) 
}

implementation A {    // or actually: extension A {
    func myFuncA(parameter1: Int, parameter2: Int) {
        print("I'm a default implementation. Sum: \(parameter1+parameter2)")
    }
}

class MyClass: A {
   func myFuncA(param1: Int, param2: Int) {
      // there would be no warning, that this doesn't conform to the protocol
   }
}

Therefor I would suggest to force the need of the overwrite keyword:

protocol A {
   func myFuncA(parameter1: Int, parameter2: Int) 
}

implementation A {   // or actually: extension A {
     func myFuncA(parameter1: Int, parameter2: Int) {
        print("I'm a default implementation. Sum: \(parameter1+parameter2)")
     }
}

class MyClass: A {
    overwrite func myFuncA(param1: Int, param2: Int) {
       // now there would be an error, that the parameters are wrong
    }
}

If you wouldn't use the overwrite keyword, the compiler would complain, that there is a default implementation for this method.

Cons

The big problem of this is, that it will break up old code and Xcode would need to refactor all of the old code.

Small solution

The smaller "solution" could be to implement a compiler warning, if there is a typo in the parameters as there is one if you just implement a protocol itself, for example in this code:

protocol A {
	func myFuncA(parameter1: Int, parameter2: Int)
}

class MyClass: A {
	func myFuncA(param1: Int, param2: Int) {
		// error: Method 'myFuncA(param1:param2:)' has different argument labels
		// from those required by protocol 'A' ('myFuncA(parameter1:parameter2:)')
	}
}

Huge -1 on the protocol extension idea. It's vastly source breaking with new semantics replacing old semantics with radical new meaning, and at little to no added expressivity or benefit.

Neutral +0 on the override requirement. It might be useful to point the user at the difference between adding conformance, and exploiting a customisation point. However, it is also source breaking, albeit much less intrusively.

This comes up often enough that it probably ought to be added to the list of commonly rejected ideas (which, in fact, I think has been suggested already somewhere). See the following links:

4 Likes

Let us try to grasp the issue once again.
Protocols are different in nature. For similar reasons, the inability to inherit from a struct in Swift is not an inconsistency either. There is a huge problem with extending protocols with new requirements. Whether requirements themselves should be considered functionality or not is moot, but extensions in Swift are dynamic and retroactive, which means that anyone can extend a public type from anywhere else, and subtypes also get the functionality. If you could extend a public protocol with new requirements, there would be practically no way to statically guarantee that a type conforms to the protocol. This breaks type safety – one of the fundamental features of Swift.
Suppose you extend Sequence with new requirements in your library. What happens to all the Standard Library types that already conform to Sequence?

  • Your program breaks at run-time if you decide to dynamically use your new requirement on a Standard Library type that conforms to Sequence. Note that it does not necessarily have to be the Standard Library: you might be using another library which also decided to add new requirements to Sequence, or a combination of them. If you think back at how complex protocol-oriented programming can get and the amount of protocols involved in your everyday Swift programming experience, you may realize this starts to look like a complete mess.
  • OK, what if we disallow to use any external conformances when we retroactively extend a protocol with new requirements? Although feasible in words, the motivation for such a groundbreaking change is yet to be seen. More importantly, this will not solve the problem you rise in the original thread. What you need is not a major protocol overhaul, but a way to express the intent to implement a requirement, so that the compiler could help you out with typo corrections and accidental witness misses. We already have such a modifier for inheritance-based polymorphism – override.
7 Likes

It would be interesting (and theoretically possible) to add new requirements with default implementations. This would allow types that know about the requirements to customize the implementation, but preserve type safety for types that do not.

1 Like

Yes, it is practically possible, by declaring a sub-protocol:

// add new requirements
protocol MySequence: Sequence {
    func makeCoffe()
}

// with default implementations
extension MySequence {
    func makeCoffe() { ... }
}

// This would allow types that know about the requirements
// to customize the implementation
extension Dictionary: MySequence { }
extension Array: MySequence {
    func makeCoffe() { ... }
}
["foo": "bar"].makeCoffe() // default implementation
[1, 2, 3].makeCoffe()      // custom implementation

// but preserve type safety for types that do not
Set([1, 2, 3]).makeCoffe() // does not compile
4 Likes