This is an interesting demonstration of a hard edge of the language. It's not because Swift holds some sort of grudge against dynamic dispatch, but rather because it's trying to reconcile some subtly incompatible design goals:
1. Adding a default implementation for a protocol requirement shouldn't stop existing types that correctly conform to that protocol from compiling. This is a reasonable user expectation and makes protocol composition a more powerful feature.
2. "Retroactive conformance" should be possible: someone should be able to conform a type they didn't write to a protocol of their own creation. It's a nifty feature that makes Swift extensions very powerful but also sometimes difficult to reason about.
3. Classes should inherit protocol conformances from their superclasses. As some of the Swift core team members have explained, this is not something that absolutely had to be the case, but it seemed sensible initially.
Points (1) and (2) preclude the language from requiring the `override` keyword for the subclass `LazyGreeter` to override a default implementation of a protocol requirement not itself overridden in the superclass `BaseGreeter`, since that would mean that (1) and/or (2) would become impossible. Getting rid of (3) would remove the surprising behavior but it is rather late in the evolution of the language to make such a large change.
The way to reason about this behavior is to consider what are intended "customization points" in the language. It isn't really covered in The Swift Programming Language, but (although not intuitive) it's a fairly teachable concept:
A separate but similar issue exists with the distinction between default implementations of protocol requirements (methods declared in both a protocol and a protocol extension) versus extension methods (methods not declared in a protocol but found in a protocol extension). The former is dynamically dispatched and is considered a customization point for conforming types, whereas the latter can only be shadowed by conforming types but is not a customization point. As described here: https://nomothetis.svbtle.com/the-ghost-of-swift-bugs-future
(The Swift standard library actually uses this concept to its advantage. For instance, the inequality operator != is not a customization point for the Equatable protocol and is implemented in an extension method, guaranteeing that !(a == b) is always equivalent to (a != b) in the generic context.)
When implementing subclasses of a third-party open class, only open methods are customization points. In that case, Swift provides compile-time checking to prohibit overriding non-open public methods. This is possible because Swift does not support "retroactive inheritance" from a superclass as it does retroactive conformance to a protocol. Also, the language allows later versions of a third-party open class to stop your existing subclasses from compiling because of a conflict between a later-added superclass method and a pre-existing method in your subclass that now lacks the `override` keyword. In other words, the compiler-enforced prohibition is possible because points (1) and (2) above were not design goals for subclassing as they were for protocol conformance.
In the case illustrated by the present article, where protocols are used in class hierarchies, a third notion of customization points arises. As demonstrated here, a protocol requirement not overridden by a base class is not a customization point for a subclass. The protocol requirement can be shadowed by the subclass but not overridden. Consistent with the design goals enumerated above, this allows for the vendor of a library protocol to provide additional default implementations at a later point without breaking user code. (But if the vendor of a conforming superclass then implements an overriding implementation, your subclass would cease to compile just as it would for any other conflict between superclass methods and subclass methods.)
> Adding a default implementation for a protocol requirement shouldn't stop existing types that correctly conform to that protocol from compiling.
I would disagree with that; adding that default implementation is not a simply internal change. It flat-out changes compilibility; it makes invalid code into valid code:
protocol P {
func f()
}
class C : P {} // Error!
---
protocol P {
func f()
}
extension P {
func f() {}
}
class C : P {} // Okay
Given the trap demonstrated here, the converse should hold as well. Doubly so in light of the ways the compiler (mostly helpfully) enforces various aspects of subclassing, as you pointed out.
In many ways, protocols (right now at least) feel like Swift having its cake but not eating it: strictness floats around them in ways that do not help developers (ugh, PATs!) but is absent where it would:
// This all compiles without any warnings?!
protocol P {
var i: Int { get set }
}
protocol Q : P {
var i: Int { get }
}
class C : Q, P {
var i = 10
}
// Now make a protocol that inherits from another,
// where both are class/AnyObject constrained
//---
struct S : Hashable { // Swift 4.1, synthesized
let s: String
}
extension S : Equatable {
static func == (lhs: S, rhs: S) -> Bool {
// Hashable semantics, smashable semantics. Hold my beer.
return lhs.s.first == rhs.s.first
}
}
1. Adding a default implementation for a protocol requirement shouldn't stop existing types that correctly conform to that protocol from compiling. This is a reasonable user expectation and makes protocol composition a more powerful feature.
2. "Retroactive conformance" should be possible: someone should be able to conform a type they didn't write to a protocol of their own creation. It's a nifty feature that makes Swift extensions very powerful but also sometimes difficult to reason about.
3. Classes should inherit protocol conformances from their superclasses. As some of the Swift core team members have explained, this is not something that absolutely had to be the case, but it seemed sensible initially.
Points (1) and (2) preclude the language from requiring the `override` keyword for the subclass `LazyGreeter` to override a default implementation of a protocol requirement not itself overridden in the superclass `BaseGreeter`, since that would mean that (1) and/or (2) would become impossible. Getting rid of (3) would remove the surprising behavior but it is rather late in the evolution of the language to make such a large change.
The way to reason about this behavior is to consider what are intended "customization points" in the language. It isn't really covered in The Swift Programming Language, but (although not intuitive) it's a fairly teachable concept:
A separate but similar issue exists with the distinction between default implementations of protocol requirements (methods declared in both a protocol and a protocol extension) versus extension methods (methods not declared in a protocol but found in a protocol extension). The former is dynamically dispatched and is considered a customization point for conforming types, whereas the latter can only be shadowed by conforming types but is not a customization point. As described here: https://nomothetis.svbtle.com/the-ghost-of-swift-bugs-future
(The Swift standard library actually uses this concept to its advantage. For instance, the inequality operator != is not a customization point for the Equatable protocol and is implemented in an extension method, guaranteeing that !(a == b) is always equivalent to (a != b) in the generic context.)
When implementing subclasses of a third-party open class, only open methods are customization points. In that case, Swift provides compile-time checking to prohibit overriding non-open public methods. This is possible because Swift does not support "retroactive inheritance" from a superclass as it does retroactive conformance to a protocol. Also, the language allows later versions of a third-party open class to stop your existing subclasses from compiling because of a conflict between a later-added superclass method and a pre-existing method in your subclass that now lacks the `override` keyword. In other words, the compiler-enforced prohibition is possible because points (1) and (2) above were not design goals for subclassing as they were for protocol conformance.
In the case illustrated by the present article, where protocols are used in class hierarchies, a third notion of customization points arises. As demonstrated here, a protocol requirement not overridden by a base class is not a customization point for a subclass. The protocol requirement can be shadowed by the subclass but not overridden. Consistent with the design goals enumerated above, this allows for the vendor of a library protocol to provide additional default implementations at a later point without breaking user code. (But if the vendor of a conforming superclass then implements an overriding implementation, your subclass would cease to compile just as it would for any other conflict between superclass methods and subclass methods.)