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

Xiaodi Wu xiaodi.wu at gmail.com
Sat Feb 11 13:56:17 CST 2017

On Sat, Feb 11, 2017 at 1:50 PM, Matthew Johnson <matthew at anandabits.com>

> On Feb 11, 2017, at 12:40 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
> On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <matthew at anandabits.com>
> wrote:
>> Sent from my iPad
>> On Feb 10, 2017, at 9:48 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>> On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <
>> 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/piper
>>> mail/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/LibraryEvol
>>> ution.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.

Perfect; I'm on board with that rationale.

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

You're quite right. I was clearly thinking more clearly yesterday than

> 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!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170211/bfe74873/attachment.html>

More information about the swift-evolution mailing list