Pre-Pitch: Explicit protocol fulfilment with the 'conformance' keyword

Hello,

I'd like to test an idea: the introduction of a new keyword in front of declarations: conformance.


EDIT - the pitch is now located at: Explicit Protocol Fulfillment.

The original post is below:


The conformance keyword means: "this declaration fulfills 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 P {
    func foo()
}

struct S1: P {
    func foo() { ... } // OK
}

struct S3: P {
    conformance func foo() { ... } // OK
}

struct S4: P {
    // error: function fooo() does not fulfill any protocol requirement.
    conformance func fooo() { ... }
}

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 problem appears when the rules of type inference miss the protocol requirement.

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

protocol Measurable {
    var length: Double { get }
}

extension Measurable {
    var length: Double { 0 }
}

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

// Prints 0.0 instead of 1.0
func test(_ p: some Measurable) { print(p.x) }
test(Meter()) 

The second problem appears when protocols evolve in time, and change their requirements. For example, this can happen with libraries when they bump their major version and introduce breaking changes. When the user upgrades the library, the compiler won't provide any help for updating methods and removing the ones that have become useless:

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

struct HelloWorld: Program {
    // Dead code
    func crash() { fatalError("Bye") }
    
    // Needs to be updated
    func run() { print("Hello World!") }
}

Note that in both examples above, the actual intent of the programmer can not be determined from looking at the code alone. It is impossible to tell if length and run() are intended to fulfill protocol requirements, or just happen to fulfill protocol requirements. This explains why it is impossible to address those problems in the current state of the language.

Yet both problems do exist, for programmers whose intent is to fulfill protocol requirements.

Proposed Solution

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

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

struct Meter: Measurable {
    // error: property 'length' does not fulfill any protocol requirement.
    // 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 'crash()' does not fulfill any protocol requirement.
    conformance func crash() { fatalError("Bye") }
    
    // error: function 'run()' does not fulfill any protocol requirement.
    conformance func run() { print("Hello World!") }
}

Those compiler errors help the user apply the proper fixes (solve type inference ambiguities, remove obsolete functions, and update the modified requirements):

struct Meter: Measurable {
    conformance let length: Double = 1
}

struct HelloWorld: Program {
    conformance func run(arguments: [String]) { print("Hello World!") }
}

Detailed Design

The conformance keyword has the compiler search for a matching declaration in all statically known conformances at the location it is used. If no such declaration exists, the compiler emits an error.

Here are a few examples:

protocol P1 { func foo() }

// =====

struct S1: P1 {
    // OK: matches P1.foo()
    conformance func foo()
}

// =====

struct S2 {
    // OK: matches P1.foo()
    conformance func foo()
}
extension S2: P1 { }

// =====

struct S3 { }
extension S3: P1 { }
extension S3 {
    // OK: matches P1.foo()
    conformance func foo()
}

// =====

protocol P2: P1 { }
struct S4: P2 {
    // OK: matches P1.foo()
    conformance func foo()
}

// =====

protocol P3: P1 { }
extension P3 {
    // OK: matches P1.foo()
    conformance func foo() { ... }
}

// =====

protocol P4 { }
extension P4 {
    // error:  function 'foo()' does not fulfill any protocol requirement.
    conformance func foo() { ... }
}

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.

  • 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 here, because when Q.foo is compiled, the P protocol is not statically known to be conformed to.

    protocol P { func foo() }
    protocol Q { }
    extension Q { func foo() { } } // <-
    struct T: P, Q { }
    

Despite those limitations, the conformance keyword 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(ProtocolName, Requirement):

    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

  • @conformance instead of conformance.

    Since override is a keyword, and fulfills a similar purpose, conformance was preferred.

  • 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 fulfilment, and become a breaking change. Note: I'm not quite sure if adding a protocol requirement is already considered as a breaking change today or not.
  • Requiring the name of the fulfilled protocol.

    This possibility is described in the future directions. But the feature already brings a lot of value, even without any explicit protocol name. After all, override does not tell which superclass is overridden.

43 Likes

That would be a welcome change! +1

Here's another motivating example and a common gotcha – conformance should also catch it when trying to override what's only an extension method.

protocol P {
    func foo()
    //func bar()  // <- Uncommenting this would fix the error.
}

extension P {
    func bar() { ... }
}

struct S: P {
    conformance func foo() { ... } // OK

    // error: function bar() does not fulfill any protocol requirement.
    conformance func bar() { ... }
}
5 Likes

In both of these cases, the desired near-miss diagnostics already trigger when the protocol conformance is declared in its own extension (or, should—I haven’t tested the exact example, and if they don’t we should file a bug to improve the existing diagnostics).

In my view, these near-miss diagnostics are one of the really nice benefits of implementing conformances in dedicated extensions. One way of looking at it is that Swift already implements the feature you’re pitching, except that it’s spelled by nesting the member inside an extension instead of prefixing with a keyword such as conformance.

There is one key shortcoming and one key advantage of the current design as compared to your pitch that I can think of:

The shortcoming is that stored properties cannot be declared as protocol-conforming implementations in an extension; this is a limitation that I and many others have long wished to be lifted for several other reasons (my very niche own pet reason is that it will allow anything expressible as fileprivate to be spelled without using that access level). I hope and expect that same-file extensions will be able to declare stored properties before long and would want that to come to the language regardless of what other near-miss features are added.

The advantage is that extension allows the user to declare not just that a member conforms a requirement of some protocol, but rather which protocol (or, even, protocols). This is, as you note, a shortcoming of your pitched design which has already been solved by the existing design.

(Another advantage, which I don’t think needs to be dwelled on except parenthetically, is that the existing design doesn’t require any additional keyword or other syntax—it’s simply making what users already do work better by producing helpful diagnostics when user intention is recognized.)

8 Likes

Maybe this is tangential, but it seems a little weird to me that extension A: SomeProtocol { ··· } does two things that are actually more or less orthogonal: adding an extension, and adding a protocol conformance, which may or may not be satisfied by things in that particular extension.

Right now you can do silly stuff like this:

struct Thing { ··· }

extension Thing: ProtocolA {
  // things that satisfy ProtocolB
}

extension Thing: ProtocolB {
  // things that satisfy ProtocolA
}

...which I think can conceivably happen to code that grows over time and gets reorganized without enough attention to detail.

It makes me wonder if there would be value in being able to say "all members needed to satisfy protocol X are exclusively in this extension (and maybe also in the main declaration)". (I'm not suggesting all extensions should be like this; frequently it's useful to declare conformance using elements that a type already has)

5 Likes

Perhaps permit the declaration name to be qualified with the protocol type?:

protocol P {
    func foo()
}

struct S: P {
    func P.foo() { }
}

Aside from error messages, the protocol name could also support refactoring, completions, and navigation.

This doesn't help when the same declaration complies with multiple protocols, but perhaps that's an anti-pattern.

3 Likes

+1 for the pitch. As a bike shed name also consider "conforming":

conforming func foo() {...}

I wonder how we handle this situation with override and could we do the same with conformance?

2 Likes

I don't think you can have this problem. While you don't know which class or classes in the chain have a function with the same signature, you do know the one being overridden is the nearest ancestor which has such a function, of which there can only be one.

Thanks @xwu. The pitch does not rely on any code structure, such as fulfillments grouped in dedicated extensions, for several reasons:

Indeed it is not always possible to declare a fulfillment in an extension. A stored property that fulfills a protocol requirement, for example, can not be declared in an extension. But even if this would becomes possible eventually, I wouldn't rely on extensions anyway.

First, even if extension grouping could help with near-misses (it does not), it would not help with the second described problem, due to protocol evolution. I'm talking about functions that becomes useless because one requirement of a protocol has been deleted, and functions that need to update their signature. Should we start to require that extension T: P { ... } can only contain P fulfillments? Even people who group protocol conformances into a dedicated extension happen to declare more related methods inside it - and I'm not sure this would be considered "bad style".

Next, the pitch does not favor one particular programming style. Grouping fulfillments in extensions is frequent, in some social groups, but not universal:

  • Some code crunching tools (I think of jazzy) don't always produce good results when conformances are declared in an extension. I've seen myself declaring all conformances upfront, at the location of the type declaration, and provide fulfillments elsewhere. It's not a personal preference, it's a pragmatic decision based on my constraints at the moment I was writing this code:

    struct T: P { ... }
    // "Detached" P conformance
    extension T { ... }
    
  • The language does not mandate any programming style. The language comes first, style comes second, and can vary. Grouping fulfillments in an extension is not required by the language. I do not know of another language feature that requires a particular style to be enabled.

  • Not all users have the same skills, or time, to polish their code. Requiring a particular style (grouping fulfillments in an extension) for enabling language features could be seen as elitist, or ableist, or pedantic for no good reason.

  • If the compiler would help solving the problems described in the pitch by emitting warnings or errors, based on extension grouping, then a simple move of code (copy and paste) would create or resolve this diagnostic. I'm afraid this would bring more confusion than good: how can the same identical function be correct here, and incorrect there? Make you your mind, compiler!

5 Likes

Thank you @wes1. Yes, a similar idea is explored in the future directions of the pitch:

struct S: P {
    conformance(P) func foo() { }
}

Your idea is good, but I still prefer conformance:

  • With conformance, you do not have to always repeat the protocol name. This could be tedious if you have to write a dozen such methods, in order to fulfill the requirement of a protocol with a very long name.
  • With conformance, we can suggest in the future directions a path to a public version of @_implements(ProtocolName, Requirement). Your proposed syntax can not be extended in the same way.

English is not my mother tongue, and I always read override as a name when in front of func or var (you never know what the brain of a stranger will do!). Hence conformance.

If native speakers see override as a verb, then conform would be better. If an adjective, conforming will certainly do. I'll let the English-native crowd choose, here :-)

3 Likes

+1 for the pitch

As a future direction, maybe we can satisfy protocol requirements by annotating dynamicMemberLookup subscripts with the conformance keyword.

protocol Requirement {
    var someProperty: Double { get }
    func someMethod(argument: Int) -> Bool
}

@dynamicMemberLookup
class Implementation: Requirement {
    conformance subscript(dynamicMember dynamicMember: String) -> Any { /* return something accordingly */ }
    conformance subscript(dynamicMember dynamicMember: String) -> Callable { Callable() }
}

@dynamicCallable
class Callable {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) -> Any { /* return something accordingly */ }
}

6 Likes

Pitch was edited with a new "The conformance keyword could be made able to "auto-fix" some near misses" paragraph in the future directions.

Maybe the pitch would be better if I made explicit an underlying idea regarding documentation.

The conformance keyword is pitched as optional. One does not have to use it. One has benefits using it, because it reveals some present or future mismatches between the programmer intent, and what the compiler sees. By "present mismatch", I mean typos and near-misses. By "future mismatch", I mean code that must change because a dependency has changed the definition of a protocol.

It is a tool that I initially intended for three groups of people:

  1. Programmers who are aware of the benefits of the keyword, for their own code base.
  2. Documentation writers, who would start recommending the use of the conformance keyword in the documentation of some protocols, when relevant.
  3. Clueless documentation readers, who use the conformance keyword because that's what the sample code says in the documentation. And that's as good for them as it is good for the programmers who are aware of what they are doing.

The benefits of conformance for the first group have been explained above.

The benefits of conformance for the documentation writers is two-fold:

  1. Some documentation can be simplified. The example below comes from GRDB (original), and was considered necessary due to the high risk of miss due to type-inference:

    Note: make sure the databaseSelection property is explicitly declared as [any SQLSelectable]. If it is not, the Swift compiler may silently miss the protocol requirement, resulting in sticky SELECT * requests.

    This paragraph aims at avoiding this kind of bug:

    struct Player: TableRecord {
        // MISS: '[Column]' is not '[any SQLSelectable]'
        static let databaseSelection = [Column("id"), Column("name")]
    }
    

    The conformance keyword would enter the sample code provided in the documentation. When the reader follows the advice, it is for their own benefit.

  2. Migration guides can be simplified. Again, consider this GRDB example (original):

    It is no longer possible to override persistence methods such as insert or update. Customizing the persistence methods is now possible with callbacks such as willSave, willInsert, or didDelete [...].

    [When upgrading] you have to remove the methods below from your own code base: [followed by a list of now-retired customization points].

    Of course the GRDB author is sorry to introduce breaking changes. That's why there is a migration guide. This guide would have been much easier to write if users could have used conformance before. The compiler would have shown the obsolete requirements without any ambiguity, guiding the user in the upgrade. Without it, an exhaustive list of functions that need to be updated is difficult to build (even if one groups conformances in dedicated extensions).

1 Like

What about this reunion of our ideas:

The initial pitch was: the conformance keyword looks for statically-known protocol requirements at the location it is used (and errors out if the declaration has no match).

When used inside an extension that declares a protocol conformance, we could have the conformance keyword perform requirement lookup in those protocols only.

To illustrate, consider the following program. It compiles without error with the pitch as written. But it would not with the suggested amendment:

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

struct S { }

extension S: Bar { }
extension S: Foo {
    conformance func foo() { }
    // Initial pitch: no error
    // Amended pitch: function bar() does not fulfill any 'Foo' requirement.
    conformance func bar() { }
}

I'm still not 100% convinced, because I do not like the idea that moving a declaration can make it valid or invalid. Yet this change would help solving some ambiguities that are discussed in the "Known Limits" paragraph of the pitch. The balance is difficult.

Following the same idea, we could have:

extension Foo where Self: Bar {
    // Conformance lookup performed in Foo and Bar only
    conformance func bar() { ... }
}

extension Foo {
    // Conformance lookup performed in Foo and Bar only
    conformance func bar() where Self: Bar { ... }
}

This amendment, if implemented later, would be a breaking change (since some code would stop compiling). This means it must come with the initial proposal, or not at all (making the implementation more difficult). I humbly let the community decide if it's worth it.

3 Likes

Can conformance be added in protocol extensions for default implementations? Also, I think it’d be nice if we could have conformance extensions, which I think would handle the design pattern outlined by @xwu. Overall, this feature has a lot of promise. I’m interested to see if there are benefits to compilation times using this approach. Additionally, adoption is another important question. I think conformance should become the default as it aids readability, diagnosing near misses and potentially the compiler. If we decide that this keyword should be required, then features such as autocomplete and migration strategies should be discussed to facilitate adoption.

2 Likes

Maybe

conformance extension MyType: SomeProtocol {
  func ...
  var ...
}

…could be sugar for

extension MyType: SomeProtocol {
  conformance func ...
  conformance var ...
}

… and that such a "conformance extension" would produce diagnostics if it included (non-private) members that did not conform to the protocol(s) listed?

7 Likes

This looks nice at first sight (maybe as a future direction, since this does not look required for the pitch to provide value).

Yet beware: such a conformance extension would ONLY accept declarations that fulfill requirements. This is the hard version of the "group protocol conformances in extensions" styling, because even related methods would become forbidden in such an extension:

conformance extension SortedArray: Collection {
    var first: ...  // OK
    var last: ...   // OK
    var median: ... // error: property 'median' does not fulfill any protocol requirement. 
}

To me, this is the exact point were something nice that always help turns into an ill-designed tool that frequently annoys. I let the community consider if they prefer something good or something too perfect.

As you say, @sveinhal, we'd ignore methods that do not match the visibility of the protocol:

conformance extension SortedArray: Collection {
    var first: ...  // OK
    var last: ...   // OK
    private func someHelper() { ... } // OK
}
5 Likes

This is a great pitch, and given the precedent of override, seems like maybe it was an oversight in the original Swift implementation. I like that it is opt-in for compatibility, but would also like to see a way to opt-in to require this.

I do think we should move directly to including the conforming protocol too, as it solves a lot of remaining problems and can leverage the implementation for @_implements.

Maybe besides the point for the pitch at hand, but for whatever it's worth: I actually prefer this hard line. Allow for private implementation details such as helper functions, but everything that is not part of the implementation, or privately aids the implementation, should be kept outside the extension, imho.

4 Likes

This is true. It would be tricky to distinguish between helper methods and methods that are actually intended to conform to another protocol.

I have had cases where I have two protocols that are very closely related, and I ended up moving methods from one protocol to the other (kinda like table view data source vs delegate - the line can be a bit blurry). With such a change, any protocol-specific extensions are now incorrectly organized, but the compiler doesn't care. So I'm looking for a way to express my intent to the compiler.