- Proposal: SE-NNNN
- Authors: David Waite
- Review Manager: TBD
- Status: Awaiting review
Types implementing protocols with optional features (either through default implementations on extensions, or optional methods on objective-C protocols) do not have a way to detect typos or upstream changes to the protocol will result in behavioral changes. This proposal attempts to rectify that in a manner similar to override
on subclassed types.
Swift-evolution thread: Discussion thread topic for that proposal
There are several issues which developers appear to be running into when using protocol extensions:
- It is unclear why, when working with references to the protocol rather than a concrete type, some implementations within a protocol extension can be overridden by a concrete type, while others can not.
- It is unclear why some method signatures would give different behavior depending on whether they were invoked on a reference to the protocol rather than a reference to the concrete type
- Objective C protocols with optional methods can result in an implementation of a protocol method being unused, without compiler error, due to typos in the method signature
- A combination of a protocol change and extension change could result in an override of a protocol method by a concrete type being no longer used, without any indication to the developer that their implementation will no longer be used
A lot of these issues are similar to issues when subclassing, which has been solved by indicating intent to the compiler via an override
keyword. This proposal attempts to establish similar practices for protocols and protocol extensions. This proposal does not attempt to change runtime semantics of protocol extensions from their current behavior.
On extensions to a protocol, the keyword default
will be used to indicate that a method or computed property provides default behavior/implementation when the concrete type does not provide an implementation.
On concrete types, the keyword fulfill
will be used to indicate that a method or property fulfills a protocol requirement, similar to the override
keyword today for indicating logic overriding superclass behavior.
When writing an extension to a protocol, the keyword default
will be used to indicate that a method or property is meant to provide default behavior of a protocol implementation when that method or property is not exposed. Absence of default
on a method or property matching the signature of a protocol (or any supertype protocols) is considered a warning, which will explain that the extension method will not be used by concrete classes implementing the method/property and suggesting the use of default
. The use of default
on a method or property which is not matched by the signature of a protocol or any supertype protocols will be an error.
// Example
protocol Shape {
func draw()
func draw(to: Surface)
func rotateClockwise(by radians: Double)
func rotateCounterclockwise(by radians: Double)
func grow(by factor: Double)
}
extension Shape {
default func draw() {
draw(to: Surface.default)
}
func rotateCounterclockwise(by radians: Double) { // warning: default missing
rotateClockwise(by: -radians)
}
default shrink(by factor: Double) { // error: default specified for method not on protocol
grow(by: -factor)
}
The goal here is several-fold:
- Make it clear that an extension to a protocol is providing a default implementation, e.g. one which will be used if a concrete type does not declare the particular method/property
- Make it clear that extension methods/properties which do not specify
default
are of a different class, in particular they are not attempting to provide any default behavior for concrete types which implement the protocol - To make it more intuitive that extensions to protocols which are non-default can conflict with concrete type methods/properties, and thus make the current corner-case behavior (where you may get different functionality depending on whether you call a method signature on a protocol or a concrete type) clearer
- To provide a mechanism to catch issues in protocol extensions due to incompatible changes (such as renaming) in the protocol
- To provide a visual cue to the developer via missing
default
keyword that a method or property they thought was meant to declare default protocol behaviors is not doing so, possibly due to typo or difference in signatures
When implementing a protocol on a concrete type, it is useful to know that a method or property is meant to fulfill the requirements of said protocol. While the override
keyword has been proposed in the past as a possible way to indicate this, the semantics of the word override
are inappropriate - without knowledge of some protocol extension declaring default implementations of methods, there is no existing logic being overridden. Instead, the keyword fulfill
is proposed here to indicate a method or property is meant to fulfill a protocol requirement.
Within file scope, the protocols and any superclass of a type are determined. In either the implementation body of that type or any extensions on that type:
- If the signature of a method or property matches a superclass, the keyword
override
is required, else giving an error - If the signature of a method or property matches a protocol requirement which is not implemented by the superclass, the keyword
fulfill
is requested, giving a warning if not present that the method is fulfilling a protocol requirement and recommendingfulfill
keyword be used - If a method or property overriding a superclass also fulfills a protocol requirement, the keyword
fulfill
is optional - Use of
override
orfulfill
outside the above defined cases is an error.
class MyUserDetailViewController : MyDetailViewController {
override var details: Details { ... } // override the superclass property to change behavior
fulfill func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat { ... } // implement an optional protocol method on the UITableViewDelegate. The superclass does not implement this method
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int { ... } // override a required protocol method on UITableViewDataSource from the superclass. The keyword `fulfill` is allowed here, but optional
}
The goal here is several-fold:
- Just as
override
makes it clear that a method is changing superclass behavior,fulfill
makes it clear that a method is implementing protocol requirements - The presence of
fulfill
means that the method matches the signature of some implemented protocol - The absensce of
fulfill
makes it clear that the method is not attempting to implement some protocol - If there is a typo in the developer's declaration of a function or property meant to fulfill a protocol requirement (such as optional protocol methods in objective C),
- the presence of the "fulfill" keyword will result in a compiler error.
- the absense of the "fulfill" keyword will be a hint to the developer that the logic is not being applied toward implementing the protocol
- If a protocol changes, existing protocol implementation in a type marked
fulfill
will result in errors on compilation
This protocol is meant to clarify usage of protocols and protocol extensions within Swift code. For the purpose of source compatibility, missing the keywords default
or fulfill
in the contexts defined in the detailed design above is considered a warning.
This allows for Swift 3 code to continue compiling until the developer has supplied the appropriate keyword flagging within their code to silence errors. Adding the keywords where warnings were issued as part of a code migration tool would also be appropriate.
Due to the new signatures, code which was altered to silence the warnings for Swift 4 would likely no longer be accepted by Swift 3.
A future version of Swift could decide to make keyword usage mandatory.
This proposal affects only compilation warnings/errors as advisory to the developer, and should not have an affect on ABI.
Existing framewokrs migrated to the syntax above should not result in source or ABI level incompatibilities, as the keywords do not affect the output of the compiler. While changes which break the API of a module without this proposal will continue to do so, changes involving the signatures within a protocol will be clearer to consuming developers in resolving said API breakage.
Most of the alternatives considered have been around keyword bikeshedding. A single keyword which could be used to indicate both protocol satisfaction and superclass implementation overriding would simplify this proposal.
Yes please. Any language pedantry disadvantage is outweighed by catastrophic failure after unnoticed protocol method signature changes. Like when upgrading your codebase to newer SDK and/or language versions.