[swift-evolution] [Draft Proposal] Require `final` on protocol extension members

Kevin Ballard kevin at sb.org
Mon Jan 4 17:52:20 CST 2016


Didn't we already have a very long discussion about all of this?

In any case, I very strongly disagree with this. It's confusing, it's overloading the term `final` in a way that doesn't seem to make sense (`final` means you can't subclass or override, but there's nothing in this proposal that affects either subclassing _or_ method overriding), and it means protocol extensions that you aren't even aware of from other modules can cause your otherwise-legal type to start throwing errors.

Making this kind of change only makes any sense at all if you assume that, every single time someone implements a method that happens to look the same as a method provided by a protocol extension, they're trying to override that protocol extension method. But since protocol extension methods cannot be overridden (merely shadowed), that assumption doesn't make a lot of sense. But even if I accepted that assumption, I would still say this is an abuse of the term `final`, and I think forcing people to brand their legitimate functions with a negative-sounding term "incoherent" is also basically subtly punishing people. I mean, who wants to write code that is publicly branded as being "incoherent" code?

Also, even after reading this proposal, it's still not obvious to me how the @incoherent attribute works. The rest is a little confusing, but this attribute is very confusing all by itself.

-Kevin Ballard

On Tue, Dec 29, 2015, at 03:04 AM, Brent Royal-Gordon via swift-evolution wrote:
> I've been working on this on-and-off for a few weeks, and I've finished drafting a formal proposal. Comments welcome.
> 
> <https://github.com/brentdax/swift-evolution/blob/final-protocol-methods/proposals/0000-require-final-on-protocol-extension-methods.md>
> 
> 
> 
> # Require `final` on protocol extension members
> 
> ## Introduction
> 
> Protocol extension members which aren't listed in the protocol itself have an unusual behavior: a conforming type can implement an identically named member, but instances with the protocol's type are always statically dispatched to the protocol's implementation. This can lead to the same instance displaying different behavior when it's cast to a  protocol it conforms to. In effect, the conforming type's member shadows the protocol's, rather than overriding it. This behavior is very surprising to some users.
> 
> The lack of a warning on this is [currently considered a bug](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001861.html), but I think we should go further and cause it to be an error. However, we should also provide an escape hatch which permits conflicts in cases where they're necessary.
> 
> ## Motivation
> 
> Suppose you write a protocol and extension like this:
> 
> 	protocol Turnable {
> 	    func turning() -> Self
> 	    mutating func turn()
> 	}
> 	extension Turnable {
> 	    mutating func turn() {
> 	        self = turning()
> 	    }
> 	
> 	    func turningRepeatedly(additionalTurns: Int) -> Self {
> 	        var turnedSelf = self
> 	        for _ in 1...additionalTurns {
> 	            turnedSelf.turn()
> 	        }
> 	        return turnedSelf
> 	    }
> 	}
> 
> Now you want to write a conforming type, `SpimsterWicket`. There are three different rules about whether your type has to, or can, implement its own versions of these methods.
> 
> 1. `turning()` is a “protocol method”: it is listed in the protocol but is not included in the extension. You *must* implement `turning()` to conform to `Turnable`.
> 2. `turn()` is a “defaulted protocol method”: it is listed in the protocol but there is also an implementation of it in the extension. You *may* implement `turn()`; if you don’t, the protocol extension’s implementation will be used.
> 3. `turningRepeatedly(_: Int)` is a “protocol extension method”: it is *not* listed in the protocol, but only in the protocol extension. This is the case we are trying to address.
> 
> Currently, in case 3, Swift permits you to implement your own `turningRepeatedly(_: Int)`. However, your implementation may not be called in every circumstance that you expect. If you call `turningRepeatedly` on a variable of type `SpimsterWicket`, you’ll get `SpimsterWicket`’s implementation of the method; however, if you call `turningRepeatedly` on a variable of type `Turnable`, you’ll get `Turnable`’s implementation of the method.
> 
> 	var wicket: SpimsterWicket = SpimsterWicket()
> 	var turnable: Turnable = wicket
> 	
> 	wicket.turn()					// Calls SpimsterWicket.turn()
> 	turnable.turn()					// Also calls SpimsterWicket.turn()
> 	
> 	wicket.turningRepeatedly(5)		// Calls SpimsterWicket.turningRepeatedly(_:)
> 	turnable.turningRepeatedly(5)	// Calls Turnable.turningRepeatedly(_:)
> 
> In most parts of Swift, casting an instance or assigning it to a variable of a different type doesn’t change which implementation will be called when you put it on the left-hand side of a dot. (I’m leaving aside Objective-C bridging, like `Int` to `NSNumber`, which is really a different operation being performed with the same syntax.) If you put a `UIControl` into a variable of type `UIView`, and then call `touchesBegan()` on that variable, Swift will still call `UIControl.touchesBegan()`. The same is true of defaulted protocol methods—if you call `turn()` on `turnable`, you’ll get `SpimsterWicket.turn()`.
> 
> But this is not true of protocol extension methods. There, the static type of the variable—the type known at compile time, the type that the variable is labeled with—is used. Thus, calling `turningRepeatedly(_:)` on `wicket` gets you `SpimsterWicket`’s implementation, but calling it on `turnable`—even though it's merely the same instance casted to a different type—gets you `Turnable`’s implementation.
> 
> This creates what I call an “incoherent” dispatch, and it occurs nowhere else in Swift. In most places in Swift, method dispatch is either based on the runtime type (reference types, normal protocol members), or the design of the language ensures there’s no difference between dispatching on the compile-time type and the runtime type (value types, `final` members). But in protocol extension members, dispatch is based on the compile-time type even though the runtime type might produce different behavior.
> 
> ## Proposed solution
> 
> I propose that we:
> 
> 1. Cause Swift to emit an error when it detects this sort of conflict.
> 2. Add a mandatory `final` keyword to statically-dispatched protocol extension members, to give a textual indication that these errors will occur.
> 3. For those circumstances in which this conflict is a necessary evil, provide an attribute which can be applied to indicate that conflicts are allowed when caused by a particular conformance.
> 
> Specifics follow, though not in the order given above. In the examples below, `T` and `U` are conforming types, `P` and `Q` are protocols, and `f` is a member which, in some cases, may be a final protocol extension member.
> 
> ### Mark protocol extension members with the `final` keyword
> 
> If we are going to emit an error for these conflicts, it would be helpful to mark which members are prone to them.
> 
> I therefore propose that all protocol extension members (that is, the ones that aren't providing a default implementation for a protocol requirement) be marked with the `final` keyword. Failing to mark such a member with `final` would cause an error.
> 
> Currently, the `final` keyword is only used on classes. When applied to a class's member, it indicates that subclasses cannot override that member. I see this new use of the `final` keyword as analogously indicating that conforming types cannot customize the member. In fact, you can capture both meanings of `final` in a single statement:
> 
> > `final` declares that subtypes of this type must use this specific implementation of the member, and cannot substitute their own specialized implementation. Attempting to do so causes an error.
> 
> ### Make conflicting with a `final` protocol extension method an error
> 
> We can now define an incoherent type as one in which a `final` protocol extension member is shadowed by a member of a conforming type, as seen from any source file. (Note that methods and subscripts are only shadowed by a member with a matching signature.)
> 
> Because incoherence is caused by the interaction of two separate declarations, there are many different circumstances in which incoherence may be caused. Here are the ones I've been able to think of:
> 
> 1. Type `T` is conformed to protocol `P` in a file where member `f` is visible on both `T` and `P`.
> 2. Type `T` is conformed to protocols `P` and `Q`, which both have a final member `f`. (Note that diamond conformance patterns, where `P` and `Q` both get the same final member `f` from protocol `R`, should be permitted.)
> 3. Type `T` is extended to add a member `f` in a file where `T`'s conformance to `P` is imported from another module, and `P` has a final member `f`.
> 4. Protocol `P` is extended to add final member `f` in a file where `T`'s conformance to `P`, and the declaration of `T.f`, are both imported from other modules.
> 5. A source file imports module A, which extends protocol `P` to include final member `f`, and module B, which conforms type `T` with member `f` to conform to `P`.
> 
> It should be noted that these conflicts are tied to *visibility*. There is no conflict if the two definitions of `f` are not both visible in  the same place. For instance:
> 
> - If file A.swift extends `T` with a private member `f`, and file B.swift extends `P` with a private final member `f`, there is no conflict.
> - If module A extends `T` with an internal member `f`, and module B extends `P` with an internal final member `f`, there is no conflict.
> - If module A extends `T` with a *public* member `f`, and module B extends `P` with a public final member `f`, there is only a conflict if A and B are imported in the same file. Even if A and B are imported in different files in the same module, there is no conflict.
> 
> ### Permit conflicts with an explicit acknowledgement through an `@incoherent` attribute
> 
> In some circumstances, it may be desirable to permit a conflict, even though it causes surprising behavior. For instance, you may want to conform an existing type to a protocol where the names conflict through sheer happenstance, and you know the protocol extension method will only ever be needed in code that treats that uses the protocol's type. In those cases, you can disable the conflict error and restore the current incoherent dispatch behavior using an `@incoherent` attribute.
> 
> The `@incoherent` attribute is always tied to a particular type and protocol. It says, in essence, "I know type T conflicts with protocol P, and I want to ignore all of those conflicts and accept incoherent dispatch." Depending on where it's attached, you may have to specify more or less information in the attribute's parameters. For instance:
> 
> 	// Mark the conformance.
> 	extension T: @incoherent P {...}
> 	
> 	// Mark the extension.
> 	@incoherent(T) extension P {...}
> 	@incoherent(P) extension T {...}
> 	
> 	// Mark the import statement
> 	@incoherent(T: P) import B
> 
> ## Detailed design
> 
> ### Errors for improper use of the `final` keyword
> 
> Failing to put a `final` keyword on a protocol extension member which requires it should emit an error message along these lines:
> 
> > f must be final because it is not a requirement of P.
> 
> This error should include a fix-it which adds the `final` keyword.
> 
> Putting a `final` keyword on a defaulted protocol member is nonsensical—it essentially gives the member the semantics of a protocol extension member. We should emit an error message along these lines:
> 
> > f cannot be final because it is a requirement of P.
> 
> This error should include a fix-it which removes the `final` keyword.
> 
> ### Errors for conflicting members
> 
> As mentioned above, there are many ways to cause a conflict, and each of them needs a slightly different wording. Here's what I propose:
> 
> 1. **Type `T` is conformed to protocol `P` in a file where member `f` is visible on both `T` and `P`.** The declaration of the conformance (that is, the declaration with the `: P` clause) should be marked with an error like:
> 
>    > T cannot conform to P because T.f conflicts with final member P.f.
> 	
> 2. **Type `T` is conformed to protocols `P` and `Q`, which both have a final member `f`.** The declaration of one of the conformances should be marked with an error like:
> 
>    > T cannot conform to both P and Q because final member P.f conflicts with final member Q.f.
> 
> 3. **Type `T` is extended to add a member `f` in a file where `T`'s conformance to `P` is imported from another module, and `P` has a final member `f`.** The declaration of the concrete type extension should be marked with an error like:
> 
>    > T cannot be extended to add member f because it conflicts with final member P.f.
> 
> 4. **Protocol `P` is extended to add final member `f` in a file where `T`'s conformance to `P`, and the declaration of `T.f`, are both imported from other modules.** The declaration of the protocol extension should be marked with an error like:
> 
>    > P cannot be extended to add final member f because it conflicts with member T.f of a conforming type.
> 	
> 5. **A source file imports module A, which extends protocol `P` to include final member `f`, and module B, which conforms type `T` with member `f` to conform to `P`.** The later of the two imports should be marked with an error like:
> 
>    > B cannot be imported because final member P.f conflicts with A's T.f.
> 
> The preferred means of resolving a conflict include:
> 
> - Renaming one of the conflicting members.
> - Deleting one of the conflicting members.
> - Deleting an `import` statement which causes the conflict.
> - Adding the conflicting member to the protocol, and removing the `final` keyword, so that it becomes a defaulted protocol member.
> 
> If it is feasible to provide a fix-it suggesting one of these solutions, that should be done.
> 
> Fix-its should *not* suggest adding an `@incoherent` attribute. It is sometimes necessary to permit incoherence, but it's never desirable, because incoherence is confusing. A fix-it would encourage users to enable incoherence without actually understanding what it means, which will cause confusion.
> 
> ### Marking multiple `@incoherent` types
> 
> In places where one parameter to the `@incoherent` attribute is permitted, you can instead provide a comma-separated list to permit several different incoherences caused by the same declaration:
> 
> 	@incoherent(T, U) extension P {...}
> 	@incoherent(P, Q) extension T {...}
> 	
> 	@incoherent(T: P, U: Q) import B
> 
> ## Impact on existing code
> 
> The requirement that `final` be applied to many protocol extension methods will cause many—perhaps most—protocol extensions to stop compiling without changes. However, the changes needed—adding a keyword to the declarations of relevant members—are fairly mechanical and are fix-it guided. The migrator should add them automatically.
> 
> The conflict errors will cause a smaller number of conformance declarations, protocol extensions, or `import` statements to fail to compile. I believe that many of these cases will be bugs, and users will want to rename members to avoid them. When users do not want to rename, they can preserve the existing semantics with an `@incoherent` attribute.
> 
> Because this is primarily a safety feature, does not change runtime semantics, and, in most codebases, all changes will be purely mechanical, I believe this feature should be added to the next minor version of Swift, rather than waiting for 3.0.
> 
> ## Alternatives considered
> 
> ### Dynamically dispatch calls to protocol extension members
> 
> This would fix the underlying problem—the confusing behavior—by making protocol extension members not behave confusingly.
> 
> This would likely take a redesign of protocol witnesses to include extension methods not listed in the original protocol. It's probably not impossible—class extensions behave this way—but it's a much bigger change than what I propose, which keeps the current runtime semantics and only adds compile-time errors and keywords.
> 
> Dynamically dispatching to protocol extension members would also change the performance characteristics of these calls. Even if this change were made, we might want to allow users to apply `final` to extension methods which they want to be dispatched statically.
> 
> ### Don't provide an `@incoherent` attribute
> 
> This would improve safety by making this confusing construct completely impossible to write. However, it would also make it completely impossible to conform certain types to certain protocols or import certain combinations of modules into the same file. This seems especially unwise because previous versions of Swift have actually permitted this shadowing; code that previously compiled without even a warning could be difficult to port.
> 
> ### Mark default members instead of statically dispatched members
> 
> This would invert the keywording in a protocol extension: instead of marking the statically-dispatched members with `final`, you would mark the overridable members with `default`.
> 
> I prefer `final` because it marks the more unusual case. Users are not surprised that they can override default methods; they are surprised that they *can't* reliably override protocol extension methods. Also, as mentioned in the previous section, I could see `final` being retained even if protocol extension methods gained dynamic dispatch.
> 
> However, my preference is not terribly strong, and using `default` on the overridable methods is not a bad option.
> 
> ### Require a `final` keyword, but don't prevent conflicts with an error
> 
> The problem with this approach is that the conflict is the surprising part. It doesn't matter all that much whether protocol extension members are dispatched statically or dynamically *except* if there's a conflict; *then* you're getting into potential bug territory. The `@incoherent` keyword is what makes this mistake impossible to make accidentally; without it, this is merely a proposal to force people to annotate their protocol extensions more clearly.
> 
> ### Don't require a `final` keyword, but prevent conflicts with an error
> 
> Without the `final` keyword (or the `default` alternative mentioned above) on the extension members themselves, it's impossible to tell at a glance which members are overridable and which ones aren't. This makes predicting incoherent conformance errors an exercise in trial-and-error, or at least in visual-diffing two separate parts of the code to figure out what's going on. Without the `final` keyword, in other words, avoiding conflicts when you write your code instead of discovering them when you compile it is much more difficult.
> 
> ### Mark specific conflicting members
> 
> Rather than marking an entire conformance as permitted to allow conflicts, we could force users to mark specific conflicting members. For instance, in the `Turnable`/`SpimsterWicket` example used above, `turningRepeatedly(_:)` itself would have to be marked in some way.
> 
> The main issue I see is that, in many cases, a conformance is added in a different file or even module than the conflicting declaration, so there's no convenient place to put the attribute. This is basically the same reason there's no equivalent of `override` for implementations of protocol methods.
> 
> This would also require more declarations, but I don't necessarily think that would be a bad thing.
> 
> 
> 
> -- 
> Brent Royal-Gordon
> Architechies
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution


More information about the swift-evolution mailing list