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

That's fair, and that's your preference; my preference is the opposite - that when a type can be consolidated in to a single, 4-line declaration, without any extensions at all, it actually becomes easier to understand.

But again, these are our individual preferences. Hopefully we can agree that both are reasonable, and that it would be regrettable if the language favoured one way of organising things.

My remarks are not necessarily targeted at the conformance keyword as proposed; but at the idea that extensions already give us all we need. Since this is a "pre-pitch", I'm trying to elaborate on some of the potential use-cases so we can get a fuller understanding of the problem developers face.

Grouping things in to extensions is also an optional strategy for enabling these diagnostics, and as discussed, in some situations it can be impossible as the language is today, and in other cases, even when it is possible, it can be non-obvious.

We tend to say that writing conformances in extensions is more idiomatic, and for complex types and conformances I would agree, but it is not always the case. I hope we don't start using that as justification to disregard other, supported ways of doing things.

5 Likes

I wonder if it would be most profitable to focus on @gwendal.roue's concrete use cases here, brainstorming what can be done to improve the user experience.

The issue that a protocol requirement of existential type can have an implementation of concrete type which the user has spelled correctly but is silently inferred not to fulfill that requirement certainly warrants improvement. The issue that there is no way to deprecate or obsolete a customization point which is entirely under the control of the author also—I hope we can agree—merits consideration.

My inclination, as I've said above, is to see if we can make improvements in these areas without necessarily even requiring language-level changes that have to be driven through Evolution.

5 Likes

I bet this has been discussed more than once in the past, but as it is not that easy to find those threads, I want to link one which imo still has relevance: Introducing role keywords to reduce hard-to-find bugs.
I'd still prefer a variant of the func Protocol.reqirement scheme over another new keyword.

3 Likes

This proposal was indeed very related. Thank you for performing this search in the forum archives!

It only addresses problems for people who write protocol extensions and provide default implementations. This pitch more generally addresses problems for people who try to conform to protocols, even in the context of evolving versions of a given protocol.

The 'conformance' keyword can address part of the former pitch, though. Sample code below comes from the initial pitch from 3 years ago:

protocol ExampleProtocol {
   func thud(with value: Float)
}

extension ExampleProtocol {
    // 1: Near-miss type, detected by 'conformance': 
    // error: function does not fulfill any protocol requirement
    conformance func thud(with value: Double) { ... }
    
    // 2: Near-miss name, detected by 'conformance'
    // error: function does not fulfill any protocol requirement
    conformance func thus(with value: Float) { ... }
    
    // 3: Accidental satisfaction, intended as extra method
    func thud(with value: Float) { ... }
}

// ----

protocol ExampleProtocol {
   func thump(with value: Float) // formerly thud
}

extension ExampleProtocol {
    // 4: Orphaned default implementation after rename, detected by 'conformance': 
    // error: function does not fulfill any protocol requirement
    conformance func thud(with value: Float) { ... }
}

So "only" problem 3 is not detected: this pitch does not come with extend.

Is it expected that I, on top of writing a pitch, improving it from feedback, and allowing members of the forum to gauge the interest from the community (as the pitch phase is described in the process) - is it expected that I write a "counter-pitch", a rebuttal of this one?

This would be a funny rhetorical experiment, but I'm only half-playing here: I have expressed in many possible ways the benefits expected from this pitch, with the help of other members. I do not feel like entertaining the members of the community with an auto-rebuttal. This is not a formal essay or a dialectical writing, this is a pitch.

So now that the topic has been explored, I don't understand what should be the next phase.

I can update the text with the ideas that have been expressed, for sure. Maybe even re-pitch it - although I think it's too early to dilute this thread with another one.

But should I try to reach the next stage, which is develop the proposal? I don't quite see what is my incentive to look for a developer, or to learn to modify the compiler myself. This sounds, at this stage, like wasted energy to me - I'm a volunteer here, not a paid employee. What kind of kick should I look for?

1 Like

I'm still mulling over the implications of the pitch, but I do have one piece of concrete feedback:

This should be an attribute, not a keyword.

In other words, I believe it would be better to use this as:

struct/extension Foo: SomeProtocol {
    @conformance(SomeProtocol)
    func fooProtocolMethod() { ... }
}

... as opposed to the proposed conformance func fooProtocolMethod() { ... }.

Keywords on methods meaningfully affect the callsite of the method, and breaking these invariants is a compiler error:

  • mutating → the method can only be used at the callsite if the receiver is a var
  • throws → the method can only be used with try
  • async → the method can only be used within an async context

Edited to add: Additionally, omitting a keyword fundamentally changes how it can be used. Adding or removing conformance to a method does not appear to affect the callsite.

Attributes, on the other hand, do not affect the callsite (with a couple minor exceptions that I know of, tangented below). Instead, they provide metadata to the compiler about how it should compile the code, which is what we're describing here. We're trying to inform the compiler about how it treats the method with respect to protocol method resolution. This lines up with other attributes that get used on methods: @availability, @inlinable, @objc, @IBAction, etc

Minor exceptions

@discardableResult and is a bit of an odd one out here, since it does affect the callsite. Perhaps it should be a keyword instead:

func doFoo() -> discardable Int

Similarly, @warn_unqualified_access also affects the callsite:

struct Foo {
    @warn_unqualified_access
    func bar() { }

    init() {
        bar() // warning
        self.bar() // ok
    }
}

Since conformance does not affect the callsite of the method, it should not be a keyword, but instead the attribute @conformance.

I think it might also be useful to pass which protocol the method is for. This leaves open the door of things like:

  • explicitly annotating that a method can be used as part of the conformance of multiple protocols
  • potentially renaming protocol conformance methods by generating a stub, especially when dealing with poorly named legacy APIs (ie, @conformance(SomeProtocol.makeTheThingThatThisThingNeeds(_:)) attached for a func makeThing() method)
14 Likes

Moving towards a formalization of @_implements would be great, IMO. I think it’s also reasonable to allow progressive specificity here, so that @conformance, @conformance(SomeProtocol), and @conformance(SomeProtocol.someRequirement) would all (eventually) be valid syntax. I don’t think it’s necessary that this happen alongside the introduction of @conformance, but I think this direction is promising and whatever design we settle on should support this potential future direction.

5 Likes

Thanks Dave. I made conformance a keyword due to its similarities with override. I have no strong arguments against an @-prefixed attribute. I only had a slight opportunity to spare one more @.

I think it might also be useful to pass which protocol the method is for.

I totally agree! This idea is developed in the "future directions" section of the pitch, including a potential public version of the underscored attribute @_implements.

I'll let the community decide if providing the protocol is mandatory. This is not my gut feeling: I see a lot of value already in a plain conformance. In other words, explicit protocols address even more use cases - but there are already a lot of use cases that are covered by the naked keyword.

2 Likes

Yes! I should make this clear in the pitch. conformance has indeed no impact on the call site. I can also say that it is not "exported". I lack the proper compiler vocabulary here, but conformance, as pitched, is not present in the public interface of a module, for example. If I have to implement it myself, I'll learn the proper vocabulary here ;-)

Well, if you (or anyone else) would like to see this make it's way into Swift, someone will have to take on the work of developing the proposal and producing a prototype implementation, paid or no. Incentive hopefully arises from the desire to work in a language where the pitched feature makes your development life easier.

More broadly, it is of course the choice of the proposal author to decide which ideas get included in the actual proposal, but there is an expectation that alternatives receive consideration in the proposal text (in the Alternatives considered section). A rebuttal of a considered alternative need not amount to a full exploration of the alternative's design space, but remember that proposals are intended to be persuasive—they're making a case for why a particular feature ought to be added to Swift, and why alternatives are less desirable. Some alternatives might not need more rebuttal than "it is the author's aesthetic preference to do things this way," but that's a less compelling argument than "this alternative does not sufficiently address the issue at-hand."

4 Likes

All right, thank you @Jumhyn. So I indeed have to write an auto-rebuttal - oops, wrong wording - an exploration of exposed alternatives and the reasons why they were not preferred, in the "Alternatives Considered" section. You made that point clear, thanks for that :+1: :slightly_smiling_face:

@xwu, my last answer to you is obsolete!

1 Like

+1 for the feature :100:

This risk of incorrect behavior due to typos or lib API changes has been bothering me for a while, and I've definitively already encountered the issues you mentioned in your pitch a couple of times in the past.

In fact, I'm pretty sure there were some past comments here in the forums (or was it in bugs.swift.org back when it was a thing?) mentioning these shortcomings a while ago, and suggesting that we should need developers to use a dedicated keyword to be explicit about those. (sadly can't find a link to those past comments/bugreports right now, but I don't think it made it to pre-pitch or pitch stage, mostly comments on existing thread… will report back if I find the link)

EDIT: Found some past discussions about this here via a quick search. Didn't read them all in details, but could be relevant to check if some past info and discussions could be helpful:


As for the keyword name bike-shedding, would that make sense to reuse the required keyword for this?

1 Like

I don't think this is universally the case. Consider "override func" - it doesn't change the call site compared to "func". Or "required/convenience init" - from the call site perspective it is the same as "init". "@" symbol would be visual noise on my book.

I always put protocol requirements into respective extensions, and if you do that you'd consider "@conformance(SomeProtocol)" redundancy / noise.

+1 for "discardable T" - I really like it.

1 Like

The new version of the pitch is online: ExplicitProtocolFulfillment.md · GitHub.

I have significantly modified the initial idea, taking in account the frequent Swift practice of grouping conformances in dedicated extensions.

The pitch is generally more precise. A new "Alternatives Considered" section was added, as well as a "Recommendations for Source Code Editors" section.

Thank you all for helping refining an idea that could greatly improve our daily practice :+1:

6 Likes

I'm curious why you suggest that something like hash(into:) should not carry the conformance decoration?

Personally, I really like seeing it so that I can understand that a function's existence is to fulfill a requirement and not just there at the whim of the programmer. This is especially helpful for beginners or for any new-to-me protocol conformance.

I agree, Jared. I have removed this paragraph, that did not bring much.

1 Like

I had only a quick glance at the proposal and this thread, sorry if I missed something crucial.

I quite like the idea, but not the keyword conformance prefixing the thing being adopted; it would be pure visual noise if there were a few sprinkled here an there.

But would something like the following be more appropriate?

protocol Odo {
   // Odo things ...
}

protocol Doodle {
   // Doodle things ...
}
enum | class | struct Foo {
   ...
}

// Adopting protocols
//
extension Foo : adopt Odo {
   // Only Odo things..
   // declaring anything else will be error
}

extension Foo : adopt Doodle {
   // Only Doodle things..
   // declaring anything else will be error
}
1 Like

The question has come before, with a previous answer. The pitch discusses your idea in the Alternatives Considered section as well.

1 Like

I would cheer this on, with one amendment: the rule should be “same module,” not just “same file.” My use case is breaking stubbornly large types into smaller files, with the benefit of file-private access, when the need for a single API surface prevents them from breaking into entirely separate types.

I would be interested in helping with a proposal for this feature, should one ever be in the works.

1 Like

It’s worth noting that this proposal is very, very similar to Java’s @Override annotation — optional, but triggers an error if present — which seems to have been fairly successful in practice, especially since IDEs added it to their autocomplete templates.

It’s also worth noting that Java does not provide the “near miss” warnings that Swift provides, as @xwu points out.

It’s also also worth noting that warnings are not errors, and fail-fast handling of shifting protocol signatures may provide unique value, especially in projects where many pieces are evolving fast and protocol signature shifts are expected.

Furthermore, while the warnings are welcome, they don’t provide great coverage. Renaming seems to me an especially compelling case for conformance: a full-on rename of a protocol method with a default implementation does not trigger a compiler warning.

4 Likes