> On Feb 10, 2017, at 1:06 AM, Xiaodi Wu <xiaodi...@gmail.com> wrote:
> 
> On Thu, Feb 9, 2017 at 9:57 AM, Matthew Johnson <matt...@anandabits.com 
> <mailto:matt...@anandabits.com>> wrote:
> 
>> On Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi...@gmail.com 
>> <mailto:xiaodi...@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

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.

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

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


> 
> 
> 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@swift.org <mailto:swift-evolution@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@swift.org <mailto:swift-evolution@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
> 
> 

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to