[swift-evolution] [Pitch] consistent public access modifiers

Matthew Johnson matthew at anandabits.com
Sat Feb 11 13:37:19 CST 2017


> On Feb 11, 2017, at 7:37 AM, Karl Wagner <razielim at gmail.com> wrote:
> 
> 
>> On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> 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
>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>> 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.

On the contrary.  Protocol refinement does not have any impact on the contract between a library and its clients while allowing clients to add conforming types does impact the contract.  

If I am implementing a library I may wish to expose a protocol rather than concrete types (or maybe in addition to concrete types), while still requiring that my implementation control what types are allowed to conform to that protocol.  Jordan mentioned that there are several examples of this design intent already in Apple frameworks.

On the other hand, allowing clients to refine a library protocol and even extend my library’s types to conform to the refined protocol (if the concrete types are visible) has no impact at all on the library.  The client will not be able to modify the conformances to the library’s protocol in any way.  

I can’t think of any reason a library or its users would ever benefit by restricting clients from refining its protocols.  Can you?

> 
> 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.

There is already a desire and a plan to allow libraries to add cases in a non-breaking way.  This is a perfectly reasonable thing to do if your enum is something like a set of mutually exclusive options that clients will pass in to a library method.  Client code should generally not be switching over an enum like this anyway.     One could argue that this type need not be an enum at all - a struct with static factory methods / properties would provide the equivalent client syntax without allowing them to perform a switch.  But there is a long history of using enums for things like this and it makes the implementation cleaner.  There isn’t a good reason to disallow it.

On the other hand, if clients are expected to switch over values of an enum type, I agree with you - the library should probably make it a closed enum.  Users should not be expected to write a default clause to handle unknown future cases.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170211/1e2fcd77/attachment-0001.html>


More information about the swift-evolution mailing list