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

Xiaodi Wu xiaodi.wu at gmail.com
Fri Feb 10 21:35:08 CST 2017

On Fri, Feb 10, 2017 at 9:15 AM, Matthew Johnson <matthew at anandabits.com>

> On Feb 10, 2017, at 1:06 AM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
> 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.
> This is a good point.  But `open`, `closed` and `public` have something
> very important in common: they all have meaning with regarding the set of
> cases, subclasses or conforming types and are mutually exclusive in the
> contract they offer in this regard.
> 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`.
> You make a great point here when you say that `closed` makes additional
> guarantees to the end user, beyond `public`.  `closed` and `open` both give
> users more capabilities by placing additional burden on the library.  In a
> very real sense `closed` *does* provide more visibility to information
> about the type - it guarantees knowledge of the *complete* set of cases,
> subclasses or protocols both now and in the future (modulo breaking
> changes).  In this sense it can be said to be “more accessible than
> public”.  This does form a strict hierarchy, just not a linear one:
>             private
>                   |
>           fileprivate
>                   |
>             internal
>                   |
>              public
>             /         \
> closed              open

Yes, I think we are now on the same page as to the mental model here. As to
whether a non-linear hierarchy is desirable or not, that's a judgment call.

> I admit that the name `closed` doesn’t *sounds* more accessible.  Maybe
> there is a better name?  But `closed` is the name we have all been using
> for this contract and it offers a nice symmetry with open.  One opens the
> set of cases, subclasses, or conforming types to users.  The other closes
> off the ability of the library to add to or hide any members of the set of
> cases, subclasses or conforming types.  The symmetry is in the fact that
> they both say something about the totality of the set of cases, subclasses
> or conforming types.
> It’s also worth noting that a `public` type can become `closed` or `open`
> in a future version of a library, but once `closed` or `open` that option
> is fixed forever (modulo breaking changes).  This also suggests the
> hierarchy I visualized above.
> 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.
> They are not exactly antonyms, but if you think about this in terms of the
> set of cases, subclasses or conforming types their obvious meaning does
> make sense.  `closed` says to users: “you know the complete set”.  `open`
> says to users: “you are allowed to add to the set”.
> I suppose this suggests a possible alternative name: `complete`.  That
> would avoid the antonym relationship and maybe be more accurate.

IMHO: nah, if we're comfortable with a non-linear hierarchy as you've drawn
above, `open` and `closed` are fine; if we're not, then no renaming will
fix that. I don't think it's the name, per se; I think my main qualm is
with the necessarily non-linear nature of the hierarchy (and with the
proliferation of access levels).

> 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.
> This has the effect of defaulting to `public` behavior.  It will lead to
> unnecessary boilerplate-y annotations in our code.  One of the big drivers
> behind the decision to make `open` an access modifier is to avoid this kind
> of boilerplate-y annotation.  Why should we have to write `@closed public`
> when simply saying  `closed` (or `complete`) unambiguously communicates the
> same thing?
>> 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.
> What I meant by “neutral” is that both `open` and `public` carry the same
> syntactic weight.  The language does not make one or the other more
> convenient for the library author and requires them to make an explicit
> choice.
> 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.
> I think part of our disagreement is in the definition of opting in.  What
> *I* mean by that is that the compiler does not make a choice for the user
> because they omitted an annotation.  We require a class visible outside the
> module to chooses `open` or chooses `public`.  By making that choice it
> opts-in to one semantic or the other.  There is no "deliberate effort to
> reject a more natural choice” required at all.  All that is required is
> that intent is unambiguous.

Well here I think we have diametrically opposite views on what is opt-in.
FWIW, I do believe that when most people in the community say that Swift
makes public API commitments opt-in, they intend that statement to mean
that the user must make a deliberate effort to override the compiler
default of not making a public API commitment, not the diametrically
opposite view that the compiler has no default choice.

After all, the default access level in Swift 3 when no access modifier is
specified is `internal`; I was one of the people on this list who made that
suggestion and was overjoyed to see it adopted.

> However, the same logic you used for `open` also works for `closed` using
> the hierarchy I visualized above.  I find the logic of that hierarchy very
> convincing (regardless of the final name we choose for `closed`).

I dislike intensely the contortions I had to go through to justify `open`
as a spelling, and I'd be a little sad to see such reasoning propagate
further. But at the end of the day, I think if we go in with eyes open and
explicitly accept or reject a non-linear access modifier scheme, it'll work
out OK.

> 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/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.
>>> _______________________________________________
>>> 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/656f7524/attachment.html>

More information about the swift-evolution mailing list