> On Feb 11, 2017, at 7:56 AM, Karl Wagner <[email protected]> wrote:
> 
> 
>> On 11 Feb 2017, at 14:37, Karl Wagner <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 
>> 
>>> On 9 Feb 2017, at 00:05, 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.
>>> 
>>> 
>>> _______________________________________________
>>> swift-evolution mailing list
>>> [email protected] <mailto:[email protected]>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
>> 
>> A couple of points:
>> 
>> 1) Protocols can also be sub-typed as well as conformed to. “Open” for 
>> classes refers to sub-typing. As I understand it, you would not be able to 
>> derive a protocol from a non-open protocol. Perhaps worth mentioning.
>> 
> 
> Actually now that I read that again I’m not sure. People use marker protocols 
> to group several protocol requirements, and to write extensions on 
> collections of types based on those collections of requirements. For example:
> 
> // Module 1
> 
> closed protocol DoesSomething { 
>     func doSomething() 
> }
> 
> struct MyTypeOne: Collection, DoesSomething { /* … */ }
> struct MyTypeTwo: Collection, DoesSomething { /* … */ }
> enum MyTypeThree: DoesSomething { /* … */ }
> 
> // Module 2
> 
> // Should this be allowed? Does it inherit the “closed-ness” of DoesSomething?
> protocol CollectionWhichDoesSomething: Collection, DoesSomething {
>  /* Empty, marker protocol */ 
> }
> 
> // Since MyType{One, Two} already conform to DoesSomething, this doesn’t 
> introduce a new conformance to the closed protocol.
> // Should it be allowed, then?
> extension MyTypeOne: CollectionWhichDoesSomething {}
> extension MyTypeTwo: CollectionWhichDoesSomething {}
> 
> This could probably get better with better existential support, but write now 
> we can’t use or extend a “Collection & DoesSomething” existential. You have 
> to wrap those requirements in a protocol and add conformances to it, and you 
> might need to do that for closed protocols, too. As long as you don’t 
> introduce new conformances to that closed protocol, nothing in Module 1 would 
> break.

Yes, as I stated in my previous reply, there is no reason to disallow this.  A 
library may have good reasons for not allowing clients to add conformances.  I 
can’t think of any good reason why it would want to restrict refinements of its 
protocols.

> 
>> 2) Enums are conceptually closed - that’s the whole point behind requiring 
>> exhaustive switches. Allowing later library versions to add/remove cases is 
>> important, but adding default cases to handle case “?” sounds like a bad 
>> solution. With zero information about what is happening, and the chance that 
>> some cases may not exist any more, anything you write in such a default 
>> statement would be useless and nonsensical.
>> 
>> I think API versioning is the better way to deal with this. Version 3 of 
>> MyLibrary.MyEnum has cases { A, B, C }, and Version 3.1 has cases { A, C, D, 
>> E }. I should be able to write code which handles either complete set of 
>> cases, depending on which version of MyLibrary is installed.
> 

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

Reply via email to