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

Speaking as an actual teacher, since there’s a hypothetical teacher making an appearance here:

I’d much rather teach conformance or override or even impl than dyn. The latter feels too much like it takes the language implementer’s viewpoint: “conforms to / implements a protocol requirement” or even “is polymorphic” are all design intents; “dynamic dispatch” is an implementation detail — one which the compiler could potentially even optimize away in some situations without violating the intent!

Perhaps it’s just my “start from concepts down, not from the metal up” teaching approach, but “dynamic dispatch” is a term we introduce to students much, much later than “abstraction,” “implementation,” “interface,” and even “polymorphism.” It’s not something I’d enjoy explaining to Swift language newcomers with my teaching hat on. ¯\_(ツ)_/¯

Regardless, it seems to me the first question is just whether this feature should exist at all before we haggle too much over its name.

10 Likes

Thanks for telling your experience as an actual teacher :+1:

Regardless, it seems to me the first question is just whether this feature should exist at all before we haggle too much over its name.

I don't know how we'll get an answer to this question :sweat_smile:

1 Like

Indeed, and if it should - then in what form. I started to like this form:

or an alternative similar constructs:

conforming extension Foo: Name {
    func foo() {...} // note: no prefix
}
extension Foo: protocol Name {
    func foo() {...} // note: no prefix
}

on the following grounds:

  • it is shorter as there's no "conformance" next to every function.
  • it obviously groups protocol conformances into a block where no extra things could be (which I believe is more win than a loss).
  • that no extra thing could be there is easier to grasp, understand and teach, as obviously you can't add some random function into a block whose name specifically says it is about "protocol conformance", whilst in the "list of conformance funcs" form it is not so obvious whether you can or cannot add other functions into the mix, which makes the corresponding language ruling about it somewhat arbitrary.
4 Likes

There’s a lot to be said for this structure. The obvious downside is that it would force closely related helper functions into a separate code block, where they might be distant from their context and thus harder to understand.

1 Like

Thanks @tera. Your question has already been asked above, and the last answer still applies:

How are you going to handle conforming API's entities to my own protocols? E.g. I have a protocol ApplicationBadgeNumberAccessor with property applicationIconBadgeNumber and conform UIApplication to it, so won't it break the logic?

Hello @edu.art,

I suppose your code looks like:

protocol ApplicationBadgeNumberAccessor {
    var applicationIconBadgeNumber: Int { get set }
}

extension UIApplication: ApplicationBadgeNumberAccessor { }

You have no reason no use conformance here, because you do not provide the implementation of UIApplication.applicationIconBadgeNumber: this property is ready-made. The conformance keyword is never required. You would not have to change anything in your code. The behavior of your program would not be modified.

I know @_implements is already mentioned in the future direction, but I wonder if this would be the opportunity to unify things right now:

protocol Program {
    func run()
}

struct HelloWorld: Program {
    // OK
    @implements(Program)
    func run() { ... }
    
    // error
    @implements(Program)
    func crash() { ... }
}

and with extension:

protocol Program {
    func run()
}

struct HelloWorld { }

@implements(Program)
extension HelloWorld: Program {
    // OK
    func run() { ... }
    
    // error
    func crash() { ... }
}
1 Like

Yes, that's exactly the intention. The pitch does not use the same syntax, but it has exactly the same effect (see the Precision of conformance intents section of the pitch):

struct HelloWorld: Program {
    // OK: matches Program.run() - and only Program.run()
    conformance // instead of @implements(Program)
    func run() { ... }
    
    // error: function crash() does not fulfill any requirement of the protocol 'Program'.
    conformance // instead of @implements(Program)
    func crash() { ... }
}

The pitch introduces conformance(Program) as a future direction, in order to solve a few known limits of the pitch (which are really not frequently met).

The pitch also introduces conformance(Program.run) as a future direction, a public version of @_implements.

Now, this:

This idea comes frequently, but it remains an idea that has problems. It is mentioned in the Alternatives Considered section of the pitch, since people keep on suggesting it.

2 Likes

Good, I personally have a stronger preference for @implements(Protocol) over conformance.

2 Likes

Yes, the naming was not really discussed yet. It is surprising, considering the great love of this forum for bike-shedding :wink:

Also, no one was able to justify why an attribute (@xxx) would be better than a keyword. I am personally preferring a keyword due to the similarity of conformance with override, and the fact that attributes are syntactically heavy. This pitch introduces a token that should be very frequently used. The pitch even recommends that source code editors automatically embed the keyword during autocompletion of protocol methods. To me, this justifies something that is visually light and does not draw attention like @attributes do.

Anyway, this is the pitch phase: everybody is encouraged to question the pitched design, and suggest alternatives.

1 Like

As far as the name goes, "implements protocol" definitely has a nicer sound to my ear than "conformance protocol". I prefer the attribute because it is easier to visually distinguish it from the rest of the code. implements(Protocol) standing on its own reads a bit weird, it looks like a function call.

1 Like

implements(MyProtocol) reads well, you're right.

Yet, one value of the pitch is the ability to provide the protocol from the enclosing scope (type definition or extension), and this spares the repetition of the name of the protocol:

extension MyType: MyProtocol {
    // Both must be MyProtocol requirements (and MyProtocol only):
    conformance func foo() { ... }
    conformance func bar() { ... }
    func other() { ... }
}

Would a naked implements read as well?

extension MyType: MyProtocol {
    implements func foo() { ... }
    implements func bar() { ... }
    func other() { ... }
}

This is a little confusing to me. First, all three functions are actually implementing something, even the third one. Next, the fact that the naked keyword is related to a protocol is not obvious.

Now there are many other words that were not considered yet: fulfill, requirement, ...

Right. In my view @implements cannot be used naked. The protocol being implemented has to be specified. The attribute can be used either on methods or on extensions.

1 Like

A coding style used by many Swift developers groups requirements in a dedicated extension. This pitch acknowledges this frequent practice, and uses it as a way to reduce the visual impact of the new feature.

I agree with that, though the easy fix is to use the attribute on the methods instead of the whole extension.

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.