> On Feb 11, 2017, at 12:40 PM, Xiaodi Wu <[email protected]> wrote:
> 
> On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <[email protected] 
> <mailto:[email protected]>> wrote:
> 
> 
> Sent from my iPad
> 
> On Feb 10, 2017, at 9:48 PM, Xiaodi Wu <[email protected] 
> <mailto:[email protected]>> wrote:
> 
>> On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution 
>> <[email protected] <mailto:[email protected]>> wrote:
>> I’ve been thinking a lot about our public access modifier story lately in 
>> the context of both protocols and enums.  I believe we should move further 
>> in the direction we took when introducing the `open` keyword.  I have 
>> identified what I think is a promising direction and am interested in 
>> feedback from the community.  If community feedback is positive I will flesh 
>> this out into a more complete proposal draft.
>> 
>> 
>> Background and Motivation:
>> 
>> In Swift 3 we had an extended debate regarding whether or not to allow 
>> inheritance of public classes by default or to require an annotation for 
>> classes that could be subclassed outside the module.  The decision we 
>> reached was to avoid having a default at all, and instead make `open` an 
>> access modifier.  The result is library authors are required to consider the 
>> behavior they wish for each class.  Both behaviors are equally convenient 
>> (neither is penalized by requiring an additional boilerplate-y annotation).
>> 
>> A recent thread 
>> (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html
>>  
>> <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html>)
>>  discussed a similar tradeoff regarding whether public enums should commit 
>> to a fixed set of cases by default or not.  The current behavior is that 
>> they *do* commit to a fixed set of cases and there is no option (afaik) to 
>> modify that behavior.  The Library Evolution document 
>> (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums 
>> <https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums>)
>>  suggests a desire to change this before locking down ABI such that public 
>> enums *do not* make this commitment by default, and are required to opt-in 
>> to this behavior using an `@closed` annotation.
>> 
>> In the previous discussion I stated a strong preference that closed enums 
>> *not* be penalized with an additional annotation.  This is because I feel 
>> pretty strongly that it is a design smell to: 1) expose cases publicly if 
>> consumers of the API are not expected to switch on them and 2) require users 
>> to handle unknown future cases if they are likely to switch over the cases 
>> in correct use of the API.
>> 
>> The conclusion I came to in that thread is that we should adopt the same 
>> strategy as we did with classes: there should not be a default.
>> 
>> There have also been several discussions both on the list and via Twitter 
>> regarding whether or not we should allow closed protocols.  In a recent 
>> Twitter discussion Joe Groff suggested that we don’t need them because we 
>> should use an enum when there is a fixed set of conforming types.  There are 
>> at least two  reasons why I still think we *should* add support for closed 
>> protocols.
>> 
>> As noted above (and in the previous thread in more detail), if the set of 
>> types (cases) isn’t intended to be fixed (i.e. the library may add new types 
>> in the future) an enum is likely not a good choice.  Using a closed protocol 
>> discourages the user from switching and prevents the user from adding 
>> conformances that are not desired.
>> 
>> Another use case supported by closed protocols is a design where users are 
>> not allowed to conform directly to a protocol, but instead are required to 
>> conform to one of several protocols which refine the closed protocol.  Enums 
>> are not a substitute for this use case.  The only option is to resort to 
>> documentation and runtime checks.
>> 
>> 
>> Proposal:
>> 
>> This proposal introduces the new access modifier `closed` as well as 
>> clarifying the meaning of `public` and expanding the use of `open`.  This 
>> provides consistent capabilities and semantics across enums, classes and 
>> protocols.
>> 
>> `open` is the most permissive modifier.  The symbol is visible outside the 
>> module and both users and future versions of the library are allowed to add 
>> new cases, subclasses or conformances.  (Note: this proposal does not 
>> introduce user-extensible `open` enums, but provides the syntax that would 
>> be used if they are added to the language)
>> 
>> `public` makes the symbol visible without allowing the user to add new 
>> cases, subclasses or conformances.  The library reserves the right to add 
>> new cases, subclasses or conformances in a future version.
>> 
>> `closed` is the most restrictive modifier.  The symbol is visible publicly 
>> with the commitment that future versions of the library are *also* 
>> prohibited from adding new cases, subclasses or conformances.  Additionally, 
>> all cases, subclasses or conformances must be visible outside the module.
>> 
>> Note: the `closed` modifier only applies to *direct* subclasses or 
>> conformances.  A subclass of a `closed` class need not be `closed`, in fact 
>> it may be `open` if the design of the library requires that.  A class that 
>> conforms to a `closed` protocol also need not be `closed`.  It may also be 
>> `open`.  Finally, a protocol that refines a `closed` protocol need not be 
>> `closed`.  It may also be `open`.
>> 
>> This proposal is consistent with the principle that libraries should opt-in 
>> to all public API contracts without taking a position on what that contract 
>> should be.  It does this in a way that offers semantically consistent 
>> choices for API contract across classes, enums and protocols.  The result is 
>> that the language allows us to choose the best tool for the job without 
>> restricting the designs we might consider because some kinds of types are 
>> limited with respect to the `open`, `public` and `closed` semantics a design 
>> might require.
>> 
>> 
>> Source compatibility:
>> 
>> This proposal affects both public enums and public protocols.  The current 
>> behavior of enums is equivalent to a `closed` enum under this proposal and 
>> the current behavior of protocols is equivalent to an `open` protocol under 
>> this proposal.  Both changes allow for a simple mechanical migration, but 
>> that may not be sufficient given the source compatibility promise made for 
>> Swift 4.  We may need to identify a multi-release strategy for adopting this 
>> proposal.
>> 
>> Brent Royal-Gordon suggested such a strategy in a discussion regarding 
>> closed protocols on Twitter:
>> 
>> * In Swift 4: all unannotated public protocols receive a warning, possibly 
>> with a fix-it to change the annotation to `open`.
>> * Also in Swift 4: an annotation is introduced to opt-in to the new `public` 
>> behavior.  Brent suggested `@closed`, but as this proposal distinguishes 
>> `public` and `closed` we would need to identify something else.  I will use 
>> `@annotation` as a placeholder.
>> * Also In Swift 4: the `closed` modifier is introduced.
>> 
>> * In Swift 5 the warning becomes a compiler error.  `public protocol` is not 
>> allowed.  Users must use `@annotation public protocol`.
>> * In Swift 6 `public protocol` is allowed again, now with the new semantics. 
>>  `@annotation public protocol` is also allowed, now with a warning and a 
>> fix-it to remove the warning.
>> * In Swift 7 `@annotation public protocol` is no longer allowed.
>> 
>> A similar mult-release strategy would work for migrating public enums.
>> 
>> A different line of feedback here:
>> 
>> As per previous reply, I now think if we clarify the mental model of the 
>> access modifier hierarchy you're proposing and adopt or reject with that 
>> clarity, we'll be fine whether we go with `closed` or with `@closed`. But I 
>> don't think the source compatibility strategy you list is the most simple or 
>> the most easy to understand for end users.
> 
> I'm pretty neutral on what kind of source compatibility strategy we would 
> adopt.  I am happy to defer to the community and core team.
> 
>> 
>> - I'll leave aside closed protocols, which as per Jordan Rose's feedback may 
>> or may not have sufficient interestingness.
> 
> Jordan supported allowing protocols to have the same choice of contract that 
> classes do today. `public protocol` has the same meaning as `open class` 
> today so if we want consistency we need a breaking change.
> 
> Sure; I was specifically considering the phased introduction of `closed`. 
> It's been a while since I've thought about how to phase in a change regarding 
> public protocols and open protocols.
> 
> That said, others make good points about _conforming to_ protocols by a type 
> vs. _refining_ protocols by another protocol, and whether either or both of 
> these is more akin to subclassing a class.

This is something that was in the back of my mind for months (I’ve thought 
about this off and on since last summer).  My conclusion is that *conforming* 
is the important relationship, at least in terms of the `open`, and `closed` 
discussion.  

As I mentioned in my reply to Karl, I can’t think of any benefit that would be 
afforded to either a library or its clients by restricting refinement.  
Obviously clients get more flexibility if they *can* refine protocols defined 
by a library.  From the perspective of a library author nothing changes if a 
client refines a protocol it defines.  All of the semantics of the code in the 
library is identical either way, as is it’s options for future evolution.

> 
>> - With respect to enums, I don't think we need such a drastic whiplash in 
>> terms of what will compile in future versions. Instead, we could take a more 
>> pragmatic approach:
>> 
>> 1. In Swift 4, remove the warning (or is it error?) about `default` cases in 
>> switch statements over public enums. Simultaneously, add `closed` or 
>> `@closed` (whatever is the approved spelling) and start annotating standard 
>> library APIs. The annotation will be purely future-proofing and have no 
>> functional effect (i.e. the compiler will do nothing differently for a 
>> `closed enum` or `@closed public enum` (as the case may be) versus a plain 
>> `public enum`).
>> 2. In Swift 4.1, _warn_ if switch statements over public enums don't have a 
>> `default` statement: offer a fix-it to insert `default: fatalError()` and, 
>> if the enum is in the same project, offer a fix-it to insert `closed` or 
>> `@closed`.
> 
> Why do you say "if the enum is in the same project, offer a fix-it to insert 
> `closed`?  If the enum is in the same project we can perform an exhaustive 
> switch regardless of its public API contract (except for `open` enums if we 
> decide to add those).
> 
> Hmm, well now I'm not in favor of my own suggestion. A public enum, though it 
> may gain or lose cases in future versions, can be exhaustively switched over 
> in the present whether it's same-module or third-party. No warning or error 
> should issue on attempting to switch over a public enum without a default 
> case.

This is true for the current semantics of `public enum`.  But what I am 
suggesting is that this semantic be called `closed enum`.  `public enum` would 
allow libraries to add new cases resiliently.  This is the same semantic for 
`public enum` that is mentioned in the Library Evolution document (which spells 
my `closed enum` as `@closed public enum`).  

We have to require a default case for resilient enums because the client may 
run against a future version of the library with a new case.  I think a couple 
people have mentioned either allowing an implicit default case with `break` or 
`fatalError` to be synthesized but I am strongly opposed to this.  The only 
other option is a compiler error for a switch over a resilient enum that does 
not have a default clause.

>  
>> 3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a 
>> switch statement over a public enum doesn't have a `default` statement. Now, 
>> new syntax to extend an `open enum` can be introduced and the compiler can 
>> treat closed and public enums differently.
> 
> If the community and core team support this strategy I will also.  It seems 
> reasonable and speeds up the transition by using the point release.  That's a 
> great idea!
> 

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to