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

Xiaodi Wu xiaodi.wu at gmail.com
Fri Feb 10 01:06:50 CST 2017


On Thu, Feb 9, 2017 at 9:57 AM, Matthew Johnson <matthew at anandabits.com>
wrote:

>
> On Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
> I agree very much with rationalizing access levels, but I'm not sure I
> like this proposal for public vs. closed. How would the compiler stop me
> from editing my own code if something is closed? The answer must be that it
> can't, so I can't see it as a co-equal to open but rather simply a
> statement of intention. Therefore I think use cases for the proposed
> behavior of closed would be better served by annotations and proper
> semantic versioning.
>
>
> The most important point IMO is that they *are* co-equal in the sense that
> they define a contract between library authors, library users and the
> compiler.
>

Certainly, `open` and your proposed `closed` both represent contracts among
library authors, users, and the compiler. But so do other features that are
not access modifiers--unless your intent is to make all the (not yet
totally finalized) resilience attributes such as `@inlineable` become
access modifiers. That both `open` and the proposed `closed` are both
contracts doesn't make them both access modifiers.

To me, the reason one _could_ justify `open` being spelled like an access
modifier (which I was not supportive of, actually, though I was fine with
`public` not allowing subclassing) is that it quacks like an access
modifier in some ways. In particular, since it offers more "access" to a
class than does `public` by allowing subclassing, one can argue that it
fits at the top of a hierarchy of access levels.

As you define it, `closed` also makes additional guarantees to the end user
than does `public` by (self-)imposing restrictions on the library author.
Thus, it does not fit into a hierarchy of access modifiers where each level
is more "accessible" than the next. Put another way, my point here is that
`closed` is not the opposite of `open` in key ways, as the names might
suggest. In fact, from the perspective of a library user, both `closed` and
`open` would allow you to do more than `public`.

As you note, there are some differences in how the `closed` contract is
> supported.  But that is far less important than the meaning of the contract
> itself.
>

Since the additional guarantees of both `open` and your proposed `closed`
impose burdens on the library _author_ and offer more flexibility to the
library _user_, I feel it is highly misleading to make them co-equal but
antonyms. They are not the "opposite" of each other and the spelling would
be misleading.

Dave's comment about tools to assist with contract-compatible API evolution
> is the right way to think about this.  Of course you *can* make breaking
> changes, but we want to make it clear when you *are* making a breaking
> change, both for source and for ABI compatibility.  This will help library
> authors, but it also helps users as well as the compiler reason about code
> when we are able to offer stronger guarantees.
>

Yes, this is totally fair.


> Most notably, the behavior of public enums *already* has the API contract
> of `closed` and we do not want to remove that capability.  This proposal
> only formalizes how that contract is specified and makes it consistent
> across all kinds of types.  It *does not* introduce the idea of a closed
> semantic contract for a type.
>
> As this change didn't seem in scope for Swift 4 phase 1, I've held off on
> discussing my own thoughts on access levels. The idea I was going to
> propose in phase 2 was to have simply open and public enums (and
> protocols). I really think that completes access levels in a rational way
> without introducing another keyword.
>
>
> The reason I posted now is because formalizing this API contract for enums
> must happen before ABI is locked down, and also because there is at least
> one protocol in the standard library (`MirrorPath`) which is documented
> with the intent that it be `closed`.
>
> I understand the reluctance to introduce another keyword.  It isn’t clear
> to me what semantics you assign to `open` and `public` enums.
>
> Are you suggesting that they match the semantics defined in my proposal
> and suggesting closed enums (i.e. matching the current behavior of `public`
> enums) would require an `@closed` annotation as suggested in the Library
> Evolution document?
>

Yes, I am.


> I am opposed to this approach because it penalizes the API contract that I
> think is often the most appropriate for enums.  I strongly prefer that we
> adopt the same neutral stance that we when we introduced `open`.
>

I would not characterize `open` as a neutral stance. But that's neither
here nor there.

The problem (as I see it) with your argument is that, in general, the
following two thoughts are incompatible: (a) the additional burden of a
public API contract should be opt-in; vs. (b) there should be neutrality as
to whether or not one assumes the burden of a particular public API
contract.

Opting in means that one has made the deliberate effort of rejecting some
sort of more natural or default choice. Writing `@closed public enum` can
be easily thought of as opting in, because doing so very clearly requires
actively choosing to add something more than the alternative `public enum`.

Writing `open class` is harder to justify as opting in, because it is not
as obvious that `public class` is some sort of default. (Hence, why I did
not think that `open` should have been an access level, though I was fine
with the proposal otherwise.) The saving grace there is that, in the linear
hierarchy of access levels, `open` is two steps away from the default
access level of `internal`, whereas `public` is only one step removed. So,
one can make the passable rationalization that in choosing `open` one is
escalating from `internal` to `public` to `open`--i.e., that going the
extra step can be regarded as the act of opting in by choosing not to stop
at `public`.

As I see it, one can only be said to opt in to B (vs. an alternative A)
only to the extent that A and B are not neutral choices but rather in some
sort of hierarchy. Again, I'm unconvinced `closed` fits into a linear
hierarchy of access modifiers, and therefore I see spelling `closed` like
`open` as problematic.


On the other hand, you might be suggesting that `public` enums maintain
> their current behavior and we simply introduce `open` as a modifier that
> reserves the right for the *library* to introduce new cases while
> continuing to prohibit *users* from introducing new cases.  This approach
> has inconsistent semantics for both `public` and `open`.  These keywords
> would indicate a different API contract for enums than they do for classes
> and protocols.  In fact, `open` for enums would have a contract analagous
> with `public` for classes and protocols.  This feels like a recipe for
> confusion.  IMO, having consistent semantics for each keyword is pretty
> important.  We already have, and desire to continue to have, three distinct
> semantic contracts.  If we want keywords with consistent semantics we are
> going to have to introduce a new keyword for the third meaning.
>
>
> On Wed, Feb 8, 2017 at 17:05 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/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) 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
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170210/3497dfcf/attachment.html>


More information about the swift-evolution mailing list