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