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

I think there are two main styles of declaring conformances. The inline style has the conformance declaration within the type declaration and is convenient for certain frameworks or even simple types:

struct MyView: View {
  var body: some View { Text(“Hello World”) }
}

This example is pretty simple and given the SwiftUI it’s arguably hard to mess this up using autocompletion. But it can still be confusing to work through build errors if, for example, the return type is changed. As mentioned upthread, this is largely because the compiler can’t assume that ‘body’ is a witness and thus emits a conservative, often confusing, error. So, a simple autocompleted conformance keyword for witnesses would do the trick here.

The other extension-based conformance declarations are common for complex types with multiple properties and/or other protocol conformances. Usually, each extension implements one protocol (and the protocols it inherits from):

struct MyCollection<E> { … }

extension MyCollection: Equatable where E: Equatable { … }

extension MyCollection: Hashable where E: Hashable { … }

Adding conformance to every witness is clearly excessive. One solution would be to only allow witnesses of the protocols that are conformed to within the current extension. But this approach makes it difficult and verbose to add helper methods. As previously suggested, helper methods often have lower visibility than the protocol and adding something like noconformance for every helper would add unnecessary friction. A better solution might be to specify the access level of the protocol when declaring a conformance. Thus, the protocol’s access level will be clear and unnecessary access-level modifiers within the extension could be dropped. This practice is already enforced in some projects because it’s concise, so this feature would be built on top of an existing feature. Since the conformance has explicit visibility, any declaration with a lower access level will be clearly an implementation detail without requiring noconformance. This way, existing code using this style will mostly work even after a potential migration, since these practices are already commonplace.

As for the name of the keyword, I support impl. It’s already used in other languages and part of that is because it’s succinct. Inline conformance’s are used pervasively in many projects (including many SwiftUI projects), so having a short keyword be autocompleted in will be less intrusive.

1 Like

Sorry, maybe I'm asking question without a deep dive into the thread discussions, so conformance is supposed to only inform the programmer and do not affect the compiler?

It informs both :-)

It tells the programmer: "this method is there because it fulfills a protocol requirement".

It tells the compiler: "if you can't find a matching protocol requirement, emit an error". There is no other consequence on the compiler side. In particular, the compiled code is not affected in any way.

Things become interesting when the compiler does not find any matching requirement. The compilation fails with an error, and the programmer must modify the program so that it compiles succesfully:

  • Maybe the programmer has made a typo?

     struct TestApp: ApplicationBadgeNumberAccessor {
    -    conformance var aplicationIconBadgeNumber: Int
    +    conformance var applicationIconBadgeNumber: Int
     }
    
  • Maybe the programmer near-misses the requirement?

     struct TestApp: ApplicationBadgeNumberAccessor {
    -    conformance private(set) var applicationIconBadgeNumber: Int
    +    conformance var applicationIconBadgeNumber: Int
     }
    
  • Maybe the protocol was refactored and the requirement was removed?

     struct TestApp: ApplicationBadgeNumberAccessor {
    -    conformance var applicationIconBadgeNumber: Int
     }
    

In all cases, the programmer must fix the program so that conformance only decorates methods that actually match a protocol requirement.

There are many scenarios where the conformance keyword helps the programmer fix his program when the compiler fails finding a matching requirement. They are described in the Motivation section of the pitch.

Sure, this pitch is long, but so is the list of problems it aims at solving. Maybe you'll enjoy the full read, and even recognize some tricky situations you have met in the past?

6 Likes

Hi @Jumhyn, maybe I should have pinged you (and @xwu) explicitly, because the pitch was significantly modified with the early feedback. See the latest version.

For example, the protocols considered for conformance checking are now as few as possible, so that the programmer intent is made crystal clear. In an extension that declares a conformance, for example:

extension MyType: MyProtocol {
    // Must fulfill a MyProtocol requirement, and only MyProtocol.
    conformance func foo() { ... }
}

This is significantly different from the initial state of the pitch, where conformance extensions (extension MyType: MyProtocol { ... }) were not processed in any particular way.

The "Detailed Design" section of the pitch describes this with more details. In particular, you should appreciate that the early strict/generous dichotomy was replaced with a more grounded approach. The "Alternatives Considered" section was extended as well.

AFAIU the most tricky part is about the case when conformance was (or will be) stated somewhere else, perhaps in another file or in a superclass, or in a protocol extension, etc:

struct S {}

extension S { // perhaps in a different file
	func foo() { ... }
}

extension S: HasFooAndBar {
	func foo() is stated elsewhere
	func bar() {}
}

One solution to the "strict" approach is to have some "is stated elsewhere" explicit marker (not something we have today). And treat the missing marker as an error. Swift doesn't generally do the "duck typing", perhaps it shouldn't do it here either.

Edit: The marker could be literally empty:

extension S: HasFooAndBar {
	func foo()  // no body means it is stated elsewhere
	func bar() {}
}

Given your sample code:

struct S {}

extension S {
	func foo() { ... } // non-explicit part of HasFooAndBar conformance
}

extension S: HasFooAndBar {
	func bar() {} // non-explicit part of HasFooAndBar conformance
}

Then the rules of the Detailed Design section of the pitch tell that it is possible to use the conformance keyword on both methods (if you can and want):

struct S {}

// This extension does not declare any conformance, so all
// `conformance`-prefixed declarations must match a
// requirement in any statically known conformed protocol.
// In this sample code, HasFooAndBar is a known conformance.
extension S {
    // OK: matches HasFooAndBar.foo()
    conformance func foo() { ... }
}

// This extension declare a conformance, so all
// `conformance`-prefixed declarations must match
// a requirement of HasFooAndBar, and HasFooAndBar only:
extension S: HasFooAndBar {
    // OK: matches HasFooAndBar.bar()
    conformance func bar() {}
}

The Known Limits section of the pitch describes situations where it is actually impossible to explicitly mark a witness. The Future Directions section explores possible ways to solve it.

Anyway, the pitch does not require all witnesses to be explicitly marked with a keyword. Sometimes those witnesses are defined in source code you can't even access (there is an example right above)!

Yet the pitch allows to mark a great deal of witnesses, and suggest future directions for the missing cases. If you think the pitch as written misses some situations, please tell!

I believe we are talking about the same thing then, just don't see this mentioned in the alternatives considered section (see below).

  • There are valid programs where the fulfillment has to be added at locations where the target protocol is not statically known.

which we could be resolved via a kind of marker:

extension Foo: HasFooAndBar {
    some-marker conformance func foo() // to denote it is defined elsewhere
    conformance func bar() { ... }
}

(intriguely "some-marker" can literally be empty - i.e. no body would be a marker in itself.)

I have in mind more intriguing cases when it is not possible adding that conformance upfront. Examples:

protocol Foo1 { func foo() }
protocol Foo2 { func foo() }

class Base {
    func foo() {...} // not necessarily Foo1's or Foo2's
}

class Derived: Base, Foo1 {
    // We won't add "foo()" here as it is already provided by Base.
    // Although we haven't stated that "foo()" is "Foo1 conformance".
    // which makes it "duck typing" conformance.
    // I believe this conformance needs to be explicit, like this:

    conformance func foo()

    // to state conformance explicitly.
    // note: no body was provided here
}
  • This would break backward compatibility.

Indeed.
Not the end of the world but definitely increases the stakes.

  • Compiler performance: the conformance check has a cost.

I believe these checks (have to) happen anyway.

  • This would force programmers to use it on fortuitous fulfillments, ruining the very goal of the keyword which is to declare an intent.
  • Adding a requirement to a protocol would create an opportunity for fortuitous fulfillment, and become a breaking change.

I'm afraid I didn't get these two.

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'.
}```

I'd say "a minor inconvenience in some cases, totally fine in others". Note that we have a precedent already: you are adding an extension with a block of related methods to add a feature but you have to add that feature variables into the main type body as you can't add those to an extension.

That said I'm +1 for the pitch even in the current form as I believe it makes swift better than it is now.

Edit: edited the code sample above.

Please allow me to paraphrase your post, just to make sure I have understood:

If the author of Base does not intend the foo method to fulfill a particular requirement, then the author of Base can not use conformance indeed, and should not even use conformance(Foo) (listed in the Future Directions of the pitch).

If, on top of that, the author of Derived relies on Base.foo for its Foo conformance, then one can wonder indeed where this intent could be materialized.

And you suggest an extra conformance func foo() without any body in Derived.

Did I get this right? If yes, I'll at least extend the "Know Limits" section of the pitch.

  • This would break backward compatibility.

Indeed. Not the end of the world but definitely increases the stakes.

The pitch, as written, does not want to break backward compatibility.

When you upgrade a library, and the library adds a requirement that happens to match one of your declarations, you end up with a fortuitous fulfillment which was not intended. If the language would force you to add conformance to this declaration, then:

  • You no longer declare an intent with conformance, but you obey the compiler.
  • The library has shipped a breaking change, since you're forced to modify your code.

I'd say "a minor inconvenience in some cases, totally fine in others". Note that we have a precedent already: you are adding an extension with a block of related methods to add a feature but you have to add that feature variables into the main type body as you can't add those to an extension.

I beg to disagree, and this is not a relevant precedent. Adding a stored property inside an extension is never possible. With the conformance extension construct, the related apis are sometimes allowed (plain extension), and sometimes forbidden (conformance extension). The mere addition of conformance in front of extension may force you to find a new home for some methods. What is an "inconvenience" for you is a daunting error for other people.

The quest for designs that are as free as possible from caveats is hard, but I think it's worth it. The problems of the conformance extension construct are identified, and they are not necessary for the pitch to be useful. They did not make the cut.

How would this work for types already defined by iOS
say I want to make a protocol called BluetoothService just to be able to mock CBService from core bluetooth (to be able to mock the service in my tests). I cannot change apple's code to conform to my protocol by adding this new keyword, how would that be handled?

Hello @filiplazov,

You do not have to add this new keyword for your code to remain valid.

For more a more precise answer please check the similar question that was asked a few days ago. The response, the response to the response, and the last response as well ;-) will hopefully lift your doubts.

1 Like

You've got it exactly right. A similar use case happens here:

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

extension Foo {
    func bar() {}
}

struct S {}

extension S: Foo {
    conformance func foo() {}
}

extension S: Bar {
    // oops. func bar() conformance was not stated. I believe it should be explicit here, e.g. this:
    conformance func bar()
}

But that would be an argument against "override" if we didn't have it and were pitching it now. A new version of a library class may add a method to the base class that matches existing method in your Derived class defined in the app, so the app now has to add "override". Still we do have "derived" and tolerate this. Or you simply had a variable "foo" in your struct but the new version of the protocol this struct adheres to now wants to have "foo" method - you'd have to change your variable name to avoid the clash. We already have to deal with this today and it doesn't seem to be a big issue. Besides, it's even a good thing to have the opportunity to review / change code on such occasions - your previous version of the function might not do exactly what the new version of a protocol wants from such a named function!

protocol P {} // v1

struct S: P {
    func foo() {
        // As this is for my app purposes only I'm doing some O(n^2) complexity code here and it's fine
    }
}

protocol P { // v2
    func foo() // This needs to work in O(1) as it is now called from some library code that's O(1)
}

If that was compiled without being a breaking change - that's oops. If that's a breaking change (you'd change your code):

struct P {
    func oldFoo() { some O(n^2) that the app was using previously }
    // obviously all places in the app that were previously use foo() now use "oldFoo"

    conformance func foo() {
        // the new O(1) code
    }
}

Needless to say the very semantic of old "foo" and new "foo" could be different - all in all this is a very good reason for this library change to be a breaking change.

I guess this is to determine what makes the cut and what's not is why we have this discussion in the first place.

The pitch was updated with:

  • The mention of conformance case (so that enums can explicitly declare their cases as witnesses - see SE-0280)
  • Detailed examples where "maximum precision" (the ability of the programmer to target one and only one protocol requirement) is prevented by the language, and is thus not a goal of the pitch.
  • One more example in the "Known Limits" section: the base class example provided by @tera in this post.

I appreciate your attempts at 1. making the language able to make absolutely all witnesses explicit, without any exception, and 2. require all witnesses to be made explicit. Yet, this absolutist stance does not solve significantly more problems, and yet does introduce significant new ones, that I have detailed in previous messages. The pitch, in its current state, solves a great deal of problems without introducing new ones.

1 Like

+1 for this pitch and for the suggestion of allowing the keyword in protocol extensions (with the added benefit of providing signatures when typing).

Also provide a flag to make missing conformance keywords generate warnings.

You're talking about a compiler flag that helps finding all places where conformance could be added.

That's an idea I would support, if ever the Swift Language Workgroup decides to support this pitch, or to merge some of its ideas with the Protocol Witness Matching Mini-Manifesto by @suyashsrijan, which triggered the interest of some Core Team members.

2 Likes

Given the limitations with retroactive conformances, I think it might make sense to limit this pitch to extensions a la @implements(ProtocolName) extension Foo { }

As a native english speaker, conforming feels more natural as when you type override and then the function name following it's like you're overriding the function with that name. But conform makes it sound like you're conforming the function with that name, which doesn't really make sense being that it's the class that's being conformed not the function. conforming sounds better I think because it makes it sound like the function is the actor conforming the class, which I feel is more true to purpose.

Perhaps the only reason that override is used today is primarily as an artifact of other languages. And that if Swift developers today built the language over again from scratch, it may have actually ended up being overriding; but then again, old habits die hard.

1 Like