Sent from my iPad

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

Great!  

Of course, everything in language design is ultimately a judgement call.  My 
opinion is that a non-linear hierarchy turns out to be a natural consequence of 
the decision we made regarding `open`.  Adopting this approach has a compelling 
and consistent logic that other approaches won't have as long as `open` is an 
access modifier.

I think this is a good thing - it means the language doesn't syntactically 
favor or penalize any of `public`, `open` and `closed`.  Instead it requires an 
explicit choice between them and gives them equal syntactic weight.

A reasonable case can be made that `open` and `closed` shouldn't be access 
modifiers.  But I feel like that ship sailed when `open` was introduced.  

Personally, I feel like we made the best trade off possible and the same logic 
is relevant to the `closed` discussion.  

Most importantly, I think we should strive for consistency.  It is currently 
lacking and would get worse if `@closed` were introduced as an attribute.

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

Ok, does it work for you if everywhere I have said "opt-in" we substitute 
"explicitly stated intent"?

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

I agree.  This was the right decision.

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

If you are still uncomfortable with `open` as an access level I can understand 
why you don't want to proceed further in that direction.  

It seemed a little bit odd at first, but I think it accomplishes two very 
important things.  It requires library authors to explicitly choose a contract 
regarding how they type may be "refined" (does this work as a general term to 
refer to cases, subclasses, and protocols?).  And it does this without favoring 
or penalizing any of the options syntactically.  

This second point received quite a bit of consideration in the review of 
`open`.  There is a strong desire to avoid having Swift grow a bunch of 
annotations that lead to code feeling boilerplate-y.  Making `open` an access 
modifier accomplished both of these goals very well IMO.


> 
>>> 
>>>> 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> 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@swift.org
>>>>>> 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