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

Xiaodi Wu xiaodi.wu at gmail.com
Sat Feb 11 12:44:57 CST 2017


On Sat, Feb 11, 2017 at 12:42 PM, Matthew Johnson via swift-evolution <
swift-evolution at swift.org> wrote:

>
>
> Sent from my iPad
>
> > On Feb 11, 2017, at 7:22 AM, Rien <Rien at Balancingrock.nl> wrote:
> >
> > I admit that I have not followed all of this discussion, but as far as I
> see, we could equally well do this by calling “not-open” enum’s “final”.
> > It seems like a better fit to me.
>
> That is actually not a very good fit at all.  `final` means a class cannot
> have any subclasses.  If we applied it to enums I think the closest
> parallel would be an enum with no cases.  But this makes the enum
> uninhabited so it is not just like `final class`, but actually more like
> `abstract final class`.
>

Well, you _could_ move `final` to the cases themselves, and it would mean
the right thing: `enum Foo { final case bar, baz }`.


> >
> > Regards,
> > Rien
> >
> > Site: http://balancingrock.nl
> > Blog: http://swiftrien.blogspot.com
> > Github: http://github.com/Balancingrock
> > Project: http://swiftfire.nl
> >
> >
> >
> >
> >
> >> On 11 Feb 2017, at 14:07, Matthew Johnson via swift-evolution <
> swift-evolution at swift.org> wrote:
> >>
> >>
> >>
> >> Sent from my iPad
> >>
> >>> On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <
> swift-evolution at swift.org> wrote:
> >>>
> >>> I’m probably better describing things with some bikeshedding code, but
> feel free to criticize it as much as you’d like.
> >>>
> >>> //===========--------- Module A ---------===========//
> >>> @closed public enum A {
> >>>    case a
> >>> }
> >>>
> >>> extension A {
> >>>    case aa // error, because enum is closed
> >>> }
> >>>
> >> This is an error because you can't add cases in an extension.  I
> imagine this is how cases would be added outside the module if we allow
> `open enum` in the future.  But whether or not this is allowed *within* the
> module is a separate question that is orthogonal to `closed` and `open`.
> >>
> >>
> >>>
> >>> public func foo(a: A) {
> >>>    switch a {
> >>>    case .a:
> >>>        print("done")
> >>>    }
> >>> }
> >>>
> >>> public enum B {
> >>>    case b
> >>> }
> >>>
> >>> extension B {
> >>>    case bb // fine, because not-closed enums are extensible
> >>> }
> >>>
> >> As noted above, whether this is allowed or not *within* the module is
> orthogonal to `closed`.  *Outside* the module it would only be possible for
> enum declared `open` (if we add this feature in the future).
> >>
> >>>
> >>> public func bar(b: B) {
> >>>    switch b {
> >>>    case .b:
> >>>        print("b")
> >>>
> >>>    default: // always needed
> >>>        print("some other case")
> >>>    }
> >>> }
> >>>
> >>> // Sub-enum relationships
> >>>
> >>> // Possible even the enum A is closed, because `@closed` only
> >>> // closes the extensibility of an enum
> >>> enum SubA : A {
> >>>    case aa
> >>> }
> >>>
> >>>
> >> Now you're talking about value subtypes.  That is orthogonal.  Also,
> this syntax already has a meaning (the raw value of the enum is A) so we
> wouldn't be able to use it the way you are intending here.  Finally, it is
> misleading syntax because what you mean here is "A is a subtype of SubA"
> which is exactly the opposite of what the syntax implies.
> >>
> >> All values of A are valid values of SubA, but SubA has values that are
> not valid values of A.
> >>
> >>> // The following enum can have a sub-enum in the clients module
> >>> open enum C {
> >>>    case c
> >>> }
> >>>
> >>> public func cool(c: C) {
> >>>    switch c {
> >>>    case .c:
> >>>        print("c")
> >>>
> >>>    default: // always needed
> >>>        print("some other case")
> >>>    }
> >>> }
> >>>
> >>> @closed open enum D {
> >>>    case d
> >>> }
> >>>
> >>> public func doo(d: D) {
> >>>    switch b {
> >>>    case .b:
> >>>        print("b")
> >>>    }
> >>> }
> >>>
> >>> // The enum case is always known at any point, no matter
> >>> // where the instance comes from, right?
> >>>
> >>> let subA = SubA.aa
> >>> let otherSubA = SubA.a // Inherited case
> >>>
> >>> let a: A = subA        // error, downgrade the sub-enum to A first
> >>> let a: A = otherSubA   // okay
> >>>
> >>> foo(a: subA)           // error, downgrade the sub-enum to A first
> >>> foo(a: otherSubA)      // okay
> >>>
> >>> //===========--------- Module B ---------===========//
> >>>
> >>> // Totally fine
> >>> switch A.a {
> >>> case .a:
> >>>    print("done")
> >>> }
> >>>
> >>> extension A {
> >>>    case aa // not allowed because the enum is closed
> >>> }
> >>>
> >>> extension B {
> >>>    case bbb
> >>> }
> >>>
> >>> switch B.b {
> >>> case .b:
> >>>    print("b")
> >>> default:
> >>>    print("somethine else")
> >>> }
> >>>
> >>> bar(b: B.bbb) // fine, because the switch statement on enums without
> >>> // `@closed` has always`default`
> >>>
> >>> // Allowed because `C` is open, and open allows sub-typing, conforming
> >>> // and overriding to the client
> >>> enum SubC : C {
> >>>    case cc
> >>> }
> >>>
> >>> let subC =
> >>> SubC.cc
> >>>
> >>>
> >>> cool(c: subC) // okay
> >>>
> >>> enum SubD : D {
> >>>    case dd
> >>> }
> >>>
> >>> doo(d: D.dd)// error, downgrade sub-enum to D first
> >>>
> >>> My point here is, that we should not think of (possible) open enums as
> enums that the client is allowed to extend. That way we’re only creating
> another inconsistent case for the open access modifier. As far as I can
> tell, open as for today means “the client is allowed to subclass/override
> things from a different module”.
> >>>
> >> Yes, but subclasses are analogous to enum cases.  A subtype of an enum
> would remove cases.  I think you are misunderstanding the relationship of
> enums to classes and protocols.
> >>
> >>> And I already said it hundred of times that we should extend this to
> make open a true access modifier in Swift. That said the meaning of open
> should become:
> >>>
> >>>    • The client is allowed to sub-type (currently only classes are
> supported).
> >>>    • The client is allowed to conform to open protocols
> >>>    • The client is allowed to override open type members
> >>> This also means that extensibility is still allowed to public types.
> Public-but-not-open classes are still extensible today, which is the
> correct behavior. Extending an enum which is not closed could or probably
> should be made possible through extensions, because I cannot think of
> anther elegant way for the client to do so.
> >>>
> >> This is what `open enum` would allow.  It is the proper enum analogue
> of open classes.
> >>
> >>> That will leave us the possibility to think of sub-typing enums in the
> future (I sketched it out a little above).
> >>>
> >> Value subtyping is very interesting.  I have been working on some ideas
> around this but I want to keep this thread focused.
> >>
> >>> If I’m not mistaken, every enum case is known at compile time,
> >>>
> >> This is true today but will not always be true in the future.  That is
> in large part what this thread is about.
> >>
> >>> which means to me that we can safely check the case before allowing to
> assign or pass an instance of a sub-enum to some of its super-enum.
> (Downgrading an enum case means that you will have to write some code that
> either mutates your current instance or creates a new one which matches one
> of the super-enum cases.) Furthermore that allows a clear distinction of
> what open access modifier does and how @closed behaves.
> >>>
> >> I'm not going to comment on the rest because it is premised on a
> misunderstanding of what value subtyping is.  I'm going to share some ideas
> around value subtyping in a new thread as soon as I have a chance to finish
> putting them together.
> >>
> >>> To summarize:
> >>>
> >>>    • @closed enum - you’re not allowed to add new cases to the enum in
> your lib + (you’re allowed to create sub-enums)
> >>>    • @closed public enum - you and the client are not allowed to add
> new cases (+ the client is not allowed to create sub-enums)
> >>>    • @closed open enum - you and the client are not allowed to add new
> cases (+ the client might create new sub-enums)
> >>>    • enum - you’re allowed to add new cases (default is needed in
> switch statements) (+ you can create new sub-enums)
> >>>    • public enum - you and the client are allowed to add new cases (+
> only you are allowed to create new sub-enums)
> >>>    • open enum - you and the client are allowed to add new cases
> (everyone can create new sub-enums)
> >>> This is a lot of bike shedding of mine, and the idea might not even
> see any light in Swift at all, but I’d like to share my ideas with the
> community. Feel free to criticize them or flesh something out into
> something real. :)
> >>>
> >>> P.S.: If we had something like this:
> >>>
> >>> @closed enum X {
> >>>    case x, y
> >>>    func foo() {
> >>>     switch self {
> >>>        case .x, .y:
> >>>            print("swift")
> >>>    }
> >>> }
> >>>
> >>> enum Z : X {
> >>>    case z, zz
> >>>    override func foo() {
> >>>        // Iff `self` is `z` or `zz` then calling super will result in
> an error.
> >>>        // Possible solution: always tell the client to downgrade
> explicitly the
> >>>        // case first if there is an attempt to call super (if
> mutating),
> >>>        // or handle all cases
> >>>
> >>>        switch self {
> >>>        case .z, .zz:
> >>>            print("custom work")
> >>>        default: // or all super-enum cases
> >>>            super.foo()
> >>>        }
> >>>    }
> >>> }
> >>>
> >>>
> >>>
> >>>
> >>> --
> >>> Adrian Zubarev
> >>> Sent with Airmail
> >>>
> >>> Am 11. Februar 2017 um 04:49:11, Xiaodi Wu via swift-evolution (
> swift-evolution at swift.org) schrieb:
> >>>
> >>>> On Wed, Feb 8, 2017 at 5:05 PM, 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/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.
> >>>>
> >>>> A different line of feedback here:
> >>>>
> >>>> As per previous reply, I now think if we clarify the mental model of
> the access modifier hierarchy you're proposing and adopt or reject with
> that clarity, we'll be fine whether we go with `closed` or with `@closed`.
> But I don't think the source compatibility strategy you list is the most
> simple or the most easy to understand for end users.
> >>>>
> >>>> - I'll leave aside closed protocols, which as per Jordan Rose's
> feedback may or may not have sufficient interestingness.
> >>>> - With respect to enums, I don't think we need such a drastic
> whiplash in terms of what will compile in future versions. Instead, we
> could take a more pragmatic approach:
> >>>>
> >>>> 1. In Swift 4, remove the warning (or is it error?) about `default`
> cases in switch statements over public enums. Simultaneously, add `closed`
> or `@closed` (whatever is the approved spelling) and start annotating
> standard library APIs. The annotation will be purely future-proofing and
> have no functional effect (i.e. the compiler will do nothing differently
> for a `closed enum` or `@closed public enum` (as the case may be) versus a
> plain `public enum`).
> >>>> 2. In Swift 4.1, _warn_ if switch statements over public enums don't
> have a `default` statement: offer a fix-it to insert `default:
> fatalError()` and, if the enum is in the same project, offer a fix-it to
> insert `closed` or `@closed`.
> >>>> 3. In Swift 5, upgrade the warning to an error for non-exhaustiveness
> if a switch statement over a public enum doesn't have a `default`
> statement. Now, new syntax to extend an `open enum` can be introduced and
> the compiler can treat closed and public enums differently.
> >>>>
> >>>> _______________________________________________
> >>>> swift-evolution mailing list
> >>>> swift-evolution at swift.org
> >>>> https://lists.swift.org/mailman/listinfo/swift-evolution
> >>>
> >>> _______________________________________________
> >>> swift-evolution mailing list
> >>> swift-evolution at swift.org
> >>> https://lists.swift.org/mailman/listinfo/swift-evolution
> >> _______________________________________________
> >> swift-evolution mailing list
> >> swift-evolution at swift.org
> >> https://lists.swift.org/mailman/listinfo/swift-evolution
> >
>
> _______________________________________________
> 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/20170211/1e739c81/attachment.html>


More information about the swift-evolution mailing list