Skip to content

Instantly share code, notes, and snippets.

@groue
Last active September 28, 2022 19:05
Show Gist options
  • Save groue/202977d33e929c834673ceb17b2426c9 to your computer and use it in GitHub Desktop.
Save groue/202977d33e929c834673ceb17b2426c9 to your computer and use it in GitHub Desktop.

Explicit Protocol Fulfillment

Introduction

This pitch introduces of a new keyword in front of declarations: conformance.

The conformance keyword means: "this declaration is intended by the programmer to fulfill a protocol requirement". Its presence is never required. However, when present, the program is ill-formed unless the declaration does match a protocol requirement.

For example:

protocol Program {
    func run()
}

struct HelloWorld: Program {
    // OK
    conformance func run() { ... }
    
    // error: function crash() does not fulfill any requirement of the protocol 'Program'.
    conformance func crash() { ... }
}

Motivation

Unlike class overrides that must be prefixed with the override keyword, declarations that fulfill protocol conformances do not need any specific decoration. This creates a few problems for programmers.

The first class of problems appears when the requirement comes with a default implementation, and the confirming types unintentionally miss the intended requirement.

For example, this code compiles, but can miss the programmer's intent:

protocol Measurable {
    // Requirement declaration
    var length: Double { get }
}

extension Measurable {
    // Default implementation
    var length: Double { 0 }
}

struct Meter: Measurable {
    // Misses protocol requirement because 'Int' is not 'Double'
    let length = 1
}

func test(_ p: some Measurable) { print(p.x) }
test(Meter())         // Prints 0.0
print(Meter().length) // Prints 1

In the above example, the mismatch is due to type inference. Another opportunity for such a mismatch between the programmer's intent and the compiled program can come from existentials. Again, the code below compiles, but can miss the programmer's intent:

protocol StringBuilder {
    // Requirement declaration
    func makeString() -> StringProtocol // aka 'any StringProtocol'
}

extension StringBuilder {
    // Default implementation
    func makeString() -> StringProtocol { ... }
}

struct MyStruct: StringBuilder {
    // Misses protocol requirement because
    // 'String' is not 'any StringProtocol'
    func makeString() -> String { ... }
}

Even if Swift 5.7 has greatly improved its support for generics and existentials, the mismatch above does happen in the wild.

The second class of problems appears when protocols evolve in time, and change their requirements. This happens with libraries that introduce breaking changes, for example.

In the first example, a protocol loses one requirement:

 // MyLib drops the sourceCode() requirement
 protocol Program {
-    func sourceCode() -> String
 }
import MyLib

struct HelloWorld: Program {
    func sourceCode() -> String { ... }
}

The compiler does not tell the programmer that providing a sourceCode() implementation is no longer required in their Program-conforming types. It is not a problem, provided that the sourceCode method remains useful, even if the protocol no longer requires it.

In all other cases, it is a problem, because the program may now be in an invalid or degraded state. Maybe the programmer would want to remove their implementation of sourceCode, which has turned into dead code. Maybe the programmer has to replace this fulfillment with another one in order to preserve important behaviors.

In the next example, a protocol changes a requirement, and provides a default implementation for the updated requirement:

 // MyLib
 protocol Program {
-    func run()
+    func run(arguments: [String])
 }
 extension Program {
+    func run(arguments: [String]) { }
 }
import MyLib

struct HelloWorld: Program {
    func run() { ... }
}

The above program keeps on compiling, but can miss the programmer's intent. In case of a miss, the program has likely to be updated so that run() is renamed run(arguments:). It is also possible that run() must remain on the side of the new requirement.

A third problem appears when a programmer "overrides" a protocol method that is declared in an extension, not as a requirement:

protocol Program {
    func run()
}

extension Program {
    func runTwice() { ... }
}

struct HelloWorld: Program {
    func runTwice() { ... }
}

The above program compiles, but may miss the programmer's intent. When it does miss the intent, it is a cause of confusion and disappointment due to the inability of the "override" to be called from generic contexts.

All the situations described above are only "problems" because the actual intent of the programmer can not be determined from looking at their code alone. If the compiler knew, it could guide the programmer towards a program that matches their fulfillment intents. But the compiler does not know, and must take a conservative stance: all valid code is intended code. This creates the bad consequences described above.

The goal of this pitch is to make the programmer's intent explicit, so that the compiler is able to address those problems.

Proposed Solution

We propose that the programmer's intent to fulfill a protocol requirement can be made explicit in the program, with the conformance keyword.

The conformance keyword would create a compiler error in all our previous examples:

struct Meter: Measurable {
    // error: property 'length' does not fulfill any requirement of the protocol 'Measurable'.
    // note: protocol Measurable requires property 'length' with type 'Double'
    // note: candidate has non-matching type 'Int'
    conformance let length = 1
}

struct HelloWorld: Program {
    // error: function 'sourceCode()' does not fulfill any requirement of the protocol 'Program'.
    conformance func sourceCode() -> String { ... }
    
    // error: function 'run()' does not fulfill any requirement of the protocol 'Program'.
    conformance func run() { ... }
    
    // error: function 'runTwice()' does not fulfill any requirement of the protocol 'Program'.
    conformance func runTwice() { ... }
}

To fix these compiler errors, the programmer must remove the whole declaration, or remove the conformance keyword (giving up the intent to fulfill a requirement), or leave the conformance keyword and fix the declaration so that it actually matches the intended requirement.

Detailed Design

The conformance keyword can be placed in front of all declarations that are intended to fulfill a protocol requirement (property, method, subscript, initializer, associated types).

The code below shows all positions where conformance can be used:

struct MyCollection: RangeReplaceableCollection {
    conformance typealias Element = ...
    conformance init() { ... }
    conformance var count: Int { ... }
    conformance subscript(position: Index) -> Element { ... }
    conformance func index(after: Index) -> Index { ... }
}

protocol DecodingError {
    static var fileCorrupted: Self { get }
}

enum JSONDecodingError: DecodingError {
    conformance case fileCorrupted
}

When present, the compiler checks if the decorated declaration does indeed fulfills a requirement. If it does not, the compilation fails with an error. This error reveals and details the mismatch between the programmer's intent and the program as understood by the compiler:

struct MyCollection: Hashable {
    // error: function 'frobnicate()' does not fulfill any requirement of the protocol 'Hashable'.
    conformance func frobnicate() { }
}

The conformance keyword has no other consequence. Particularly, it has no consequence on the call site, and no consequence at runtime. It is not part of the module interface, at the API or ABI level. As a 100% optional keyword, it should not be present in the documentation shown to the api consumers (DocC, etc).

The List of Considered Protocols

In order to check if the declaration fulfills a requirement, the compiler looks for a match in a list of protocols.

This list depends on the scope (type definition or extension) that wraps the declaration, as well as a few other details described below:

If the scope declares one or several protocol conformance, then those protocols are checked:

protocol Foo { func foo() }
protocol Bar { func bar() }

// List of considered protocols: Foo, Bar
struct MyType: Foo, Bar {
    // OK: matches Foo.foo()
    conformance func foo() { }
    
    // OK: matches Bar.bar()
    conformance func bar() { }

    // error: function 'frobnicate()' does not fulfill any requirement of protocols 'Foo', 'Bar'.
    conformance func frobnicate() { }
}

// List of considered protocols: Foo, Bar
extension MyType: Foo, Bar {
    // OK: matches Foo.foo()
    conformance func foo() { }
    
    // OK: matches Bar.bar()
    conformance func bar() { }
    
    // error: function 'frobnicate()' does not fulfill any requirement of the protocols 'Foo', 'Bar'.
    conformance func frobnicate() { }
}

If the scope does not declare any conformance, then all statically known conformances are checked:

protocol Foo { func foo() }
protocol Bar { func bar() }

struct MyType: Foo { }
extension MyType: Bar { }

// List of considered protocols: Foo, Bar
extension MyType {
    // OK: matches Foo.foo()
    conformance func foo() { }
    
    // OK: matches Bar.bar()
    conformance func bar() { }
    
    // error: function 'frobnicate()' does not fulfill any requirement of protocols 'Foo', 'Bar'.
    conformance func frobnicate() { }
}

protocol MyProtocol: Foo {
    func myFunction()
}

// List of considered protocols: MyProtocol
extension MyProtocol {
    // OK: matches MyProtocol.myFunction()
    conformance func myFunction() { }
    
    // OK: matches MyProtocol.foo()
    conformance func foo() { }
    
    // error: function 'frobnicate()' does not fulfill any requirement of protocols 'Foo', 'Bar'.
    conformance func frobnicate() { }
}

Note: In the above example, the conformance keyword is a way to validate that an extension method actually defines a customization point of MyProtocol (requirement + default implementation).

For regular types, the check of all statically known conformances is justified by the Recommendations for Source Code Editors section below.

On top of the previous rules, protocols declared as conditions on Self are checked as well:

protocol MyProtocol {
    func myFunction()
}

// List of considered protocols: MyProtocol, Foo, Bar
extension MyProtocol where Self: Foo & Bar {
    // OK: matches MyProtocol.myFunction()
    conformance func myFunction() { }
    
    // OK: matches Foo.foo()
    conformance func foo() { }
    
    // OK: matches Bar.bar()
    conformance func bar() { }
    
    // error: function 'frobnicate()' does not fulfill any requirement of the protocols 'MyProtocol', 'Foo', 'Bar'.
    conformance func frobnicate() { }
}

struct Wrapper<T> { }
extension Wrapper: Foo where T: Equatable { }

// List of considered protocols: Foo
extension Wrapper where Self: Foo {
    // OK: matches Foo.foo()
    func foo() { }
}

The previous rule applies on individual declarations as well:

protocol MyProtocol { }

// List of considered protocols: MyProtocol
extension MyProtocol {
    // List of considered protocols: MyProtocol, Foo
    // OK: matches Foo.foo()
    conformance func foo() where Self: Foo { }

    // List of considered protocols: MyProtocol, Foo, Bar
    // error: function 'foo()' does not fulfill any requirement of the protocols 'MyProtocol', 'Foo', 'Bar'.
    conformance func frobnicate() where Self: Foo & Bar { }
}

struct Wrapper<T> { }
extension Wrapper: Foo where T: Equatable { }

// List of considered protocols: empty
extension Wrapper {
    // List of considered protocols: Foo
    // OK: matches Foo.foo()
    conformance func foo() where Self: Foo { }
}

Precision of Fulfillment Intents

The rules defined in the previous section are intended to cover as many possible fulfillment intents, while preserving intent precision for types that conform to several protocols.

An intent reaches maximum precision when one and only one protocol requirement can match a declaration.

Maximum precision is reached when the programmer provides distinct conformances in distinct extensions:

protocol Foo { func foo() }
protocol Bar { func bar() }

struct MyType { }

extension MyType: Foo {
    // Maximum precision: Foo requirement
    conformance func foo() { }
}

extension MyType: Bar {
    // Maximum precision: Bar requirement
    conformance func bar() { }
}

Maximum precision is also reached for default protocol implementations:

protocol MyProtocol {
    func myFunction()
}

extension MyProtocol {
    // Maximum precision: MyProtocol requirement
    conformance func myFunction() { }
}

Maximum precision can not be universally reached.

Maximum precision is not always available for stored properties, because they can not be declared in an extension:

protocol Foo { var foo: String { get }}
protocol Bar { var bar: String { get }}

struct MyType: Foo, Bar {
    // Low precision: requires Foo or Bar fulfillment
    conformance var foo: String
    // Low precision: requires Foo or Bar fulfillment
    conformance var bar: String
}

Maximum precision is not available when the witness involves several protocols:

struct MyType where Self: Foo & Bar {
    // Low precision: requires Foo or Bar fulfillment
    conformance func bar() { foo()  }
}

Maximum precision is not always available for generic classes because they can not declare the conformance to an @objc protocol in an extension:

class MyClass<T>: NSObject, NSCacheDelegate, UITableViewDataSource {
    // Low precision: requires NSCacheDelegate or UITableViewDataSource fulfillment
    conformance func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) { ... }
    
    // Low precision: requires NSCacheDelegate or UITableViewDataSource fulfillment
    conformance func numberOfSections(in tableView: UITableView) -> Int { ... }
}

The fact that maximum precision can not always be reached is not a problem. Maximum precision is not required for conformance to be useful, and address the problems described in the Motivation section.

Now, the intent declared by conformance is also read by programmers. When maximum precision is not reached, the programmer may wonder what is the intended conformance.

Yet, these doubts are often lifted. In the sample code below, which does not reach maximum precision, a certain level of familiarity with the involved protocols leaves no question about the intended conformances:

struct ID: CustomStringConvertible, Hashable, RawRepresentable {
    conformance var rawValue: String
    conformance var description: String { rawValue }
    conformance func hash(into hasher: inout Hasher) {
        hasher.combine(rawValue)
    }
}

See Known Limits and Future Directions for extra discussions about intent precision.

Recommendations for Source Code Editors

The conformance keyword is not required by the language. It can not be, because some fulfillments are genuinely unintended. Yet it brings the advantages described in the Motivation section.

This is why we recommend source code editors that can autocomplete fulfillments to automatically insert the conformance keyword. When the user selects a requirement in the list of suggested autocompletions, the user expresses an explicit intent that can safely become explicit in the autocompleted code:

protocol Foo { func foo() }

struct MyType: Foo {
    func fo^AUTOCOMPLETE
    ->
    conformance func foo() {
        <#code#>
    }

One major editor that can autocomplete fulfillments is Xcode. It its latest form, it autocompletes fulfillments regardless of the enclosing scope:

// Xcode autocomplete feature, in practice:
struct MyType { }
extension MyType: Foo { }
extension MyType {
    func fo^AUTOCOMPLETE
    ->
    func foo() {
        <#code#>
    }
}

This recommendation for editors, as well as the current behavior of Xcode, is the main reason why the conformance keyword is accepted in scopes that do not declare any protocol conformance.

Known Limits

There are situations where the conformance keyword won't be able to fully help the programmer.

  • When a type conforms to two protocols that declare the same requirement, the conformance keyword does not tell which conformance, or both, are intended:

    protocol Foo { 
        var value: Int { get set }
    }
    protocol Bar {
        var value: Int { get set }
    }
    
    struct MyType: Foo, Bar {
        // OK: matches Foo.value and Bar.value
        conformance var value: Int
    }
  • In some valid programs, the method that fulfills the conformance is not declared at a location where the compiler has enough static information to accept the conformance keyword.

    In the code below, we can't use the 'conformance' keyword, because when Bar.foo is compiled, the Foo protocol is not statically known to be conformed to.

    protocol Foo { func foo() }
    protocol Bar { }
    extension Bar { func foo() { } } // <-
    struct T: Foo, Bar { }

    See a similar situation below. We can't use the 'conformance' keyword, because when Base.foo is compiled, the Foo protocol is not statically known to be conformed to.

    protocol Foo { func foo() }
    class Base {
        func foo() { } // <-
    }
    class Derived: Base, Foo { }

We'll see some possible solutions to both caveats in the Future Directions section.

Despite those limitations, the conformance keyword as described in this document already brings more safety to the language, and more programmer confidence in the correctness of the program.

Future Directions

  1. When a type conforms to several protocols that declare the same requirement, the conformance keyword could be extended with the name of the protocol:

    protocol P { func foo() }
    protocol Q { func foo() }
    protocol R { func foo() }
    
    struct T: P, Q, R {
        // Only P and Q fulfillments were intended.
        // R fulfillment is fortuitous.
        conformance(P, Q)
        func foo() { ... }
    }
  2. The conformance keyword could pave the way for a public version of @_implements:

    struct OddCollection: Collection {
        conformance(Collection.count)
        var numberOfElements: Int
    }
  3. The conformance keyword could be made to accept statically invisible requirements, by using an explicit protocol name:

    protocol P {
        func foo()
    }
    
    protocol Q { }
    
    extension Q {
        conformance func foo() { }    // compiler error
        conformance(P) func foo() { } // OK
    }
  4. The conformance keyword could be made able to "auto-fix" some near misses:

    protocol Measurable {
        var length: Double { get }
    }
    
    extension Measurable {
        var length: Double { 0 }
    }
    
    struct Meter: Measurable {
        // OK: inferred as 'Double' instead of 'Int', due to the protocol requirement.
        conformance let length = 1
    }

Alternatives Considered

  • Addressing the problems described in the Motivation section, without any language change.

    All benefits of the conformance keyword come from the explicit intents declared by the programmer. The relevance of the diagnostics emitted by the compiler is guaranteed by the explicit intent.

    Without any explicit intent, the compiler must take a conservative stance, and consider all valid code as intended code.

    The compiler could perform educated guesses and emit warnings. But there remains the possibility of wrong guesses (genuinely fortuitous fulfillments, and intentional fulfillment misses). In case of a wrong guess, and an undesired warning, the programmer would need a way to enforce their will, and mute this warning. This would require a language change.

  • Dedicating a full extension for conformances, aka conformance extension MyType: MyProtocol { ... }.

    Since adding conformance in front of each individual declaration can be tedious, why not group such declarations inside a single conformance extension, as below?

    protocol MyProtocol {
        func foo()
        func bar()
    }
    
    conformance extension MyType: MyProtocol {
        // OK: matches MyProtocol.foo()
        func foo() { }
        
        // OK: matches MyProtocol.bar()
        func bar() { }
    }

    This idea creates a problem, because such an extension would only accept declarations that fulfill requirements. This would be an annoyance for programmers who group inside a single extension methods that are related to a conformance.

    // Just annoying
    conformance extension SortedArray: Collection {
        var first: ...  // OK
        var last: ...   // OK
        var median: ... // error: property 'median' does not fulfill any requirement of the protocol 'Collection'.
    }
  • conforming instead of conformance (source). The topic is not settled. See the answer.

  • @conformance instead of conformance.

    Since override is a keyword, and fulfills a similar purpose, conformance as a keyword was preferred over an attribute.

  • Requiring conformance, juste like we require override.

    • This would break backward compatibility.
    • Compiler performance: the conformance check has a cost.
    • This would force programmers to use it on fortuitous fulfillments, ruining the very goal of the keyword which is to declare an intent.
    • There are valid programs where the fulfillment has to be added at locations where the target protocol is not statically known.
    • Adding a requirement to a protocol would create an opportunity for fortuitous fulfillment, and become a breaking change.
  • Requiring the name of the fulfilled protocol: conformance(MyProtocol).

    This possibility is described in Future Directions. But the feature already brings a lot of value, even without any explicit protocol name: see Precision of Fulfillment Intents.

Acknowledgments

Similar ideas have already been pitched in the Swift mailing list and forums. Among them:

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