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