Skip to content

Instantly share code, notes, and snippets.

@dwaite
Last active April 7, 2017 08:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dwaite/5a10e759a0efd0d9d16e066db4291552 to your computer and use it in GitHub Desktop.
Save dwaite/5a10e759a0efd0d9d16e066db4291552 to your computer and use it in GitHub Desktop.
[SE-NNNN] Fullfill protocol requirements explicitly with extensions and types

Fullfil protocol requirements explicitly with extensions and types

Introduction

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

Motivation

There are several issues which developers appear to be running into when using protocol extensions:

  1. 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.
  2. 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
  3. 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
  4. 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.

Proposed solution

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.

Detailed design

Default implementations on protocol extensions

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:

  1. 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
  2. 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
  3. 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
  4. To provide a mechanism to catch issues in protocol extensions due to incompatible changes (such as renaming) in the protocol
  5. 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

Fulfilling a protocol on a concrete type

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 recommending fulfill keyword be used
  • If a method or property overriding a superclass also fulfills a protocol requirement, the keyword fulfill is optional
  • Use of override or fulfill 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:

  1. Just as override makes it clear that a method is changing superclass behavior, fulfill makes it clear that a method is implementing protocol requirements
  2. The presence of fulfill means that the method matches the signature of some implemented protocol
  3. The absensce of fulfill makes it clear that the method is not attempting to implement some protocol
  4. 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
  5. If a protocol changes, existing protocol implementation in a type marked fulfill will result in errors on compilation

Source compatibility

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.

Effect on ABI stability

This proposal affects only compilation warnings/errors as advisory to the developer, and should not have an affect on ABI.

Effect on API resilience

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.

Alternatives considered

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.

@stevenkramer
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment