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

Xiaodi Wu xiaodi.wu at gmail.com
Sat Feb 11 12:30:48 CST 2017


I think Matthew's point (with which I agree) is that, as enums are sum
types, adding or removing cases is akin to subclassing. You can extend a
public enum by adding methods just like you can extend a public class. But
just as you cannot subclass a public class, you should not be able to add
or remove cases from a public enum.


On Sat, Feb 11, 2017 at 8:37 AM, Adrian Zubarev via swift-evolution <
swift-evolution at swift.org> wrote:

> I have to correct myself here and there.
>
> … which would be extensible if that feature might be added to swift one
> day.
>
> Again, I see open only as a contract to *allow* sub-typing, conformances
> and overriding to the client, where extensibility of a type a story of it’s
> own.
>
>
>
> --
> Adrian Zubarev
> Sent with Airmail
>
> Am 11. Februar 2017 um 15:33:17, Adrian Zubarev (
> adrian.zubarev at devandartist.com) schrieb:
>
> It wasn’t my intention to drive to far way off topic with this. The major
> point of my last bike shedding was that I have to disagree with you about
> the potential future open enum vs. public enum and closed enum.
>
> public today does not add any guarantee to prevent the client from
> extending your type. For instance:
>
> // Module A
> public class A { public init() {} }
>
> // Module B
> extension A {
>
>     convenience init(foo: Int) {
>         print(foo)
>         self.init()
>     }
> }
>
> That also implies to me that open as an access modifier does not prevent
> extensibility.
>
> Speaking of opened enums, we really do not mean open enum to allow
> extensibility where closed enum would mean the opposite. closed or @closed
> by all the definitions I’ve read so far is what the current public means
> for enums. If this is going to be fixed to closed enum (@closed public
> enum) than what we’re currently speaking of is nothing else than public
> enum, which would be extensible if that future might be added to swift
> one day.
>
> Again, I see open only as a contract to prevent sub-typing, conformances
> and overriding, where extensibility of a type a story of it’s own.
>
> Quickly compared to protocols: public-but-not-open protocol from module A
> should remain extensible in module B. Consistently that would mean that public
> enum is the enum when we’re talking about future extensibility of that
> enum from the clients side outside your module. You simply should be able
> to add new cases directly to your enum if it’s not annotated as closed. open
> enum on the other hand makes only sense when we’d speak about sub-typing
> on enums or value types in general.
>
>
> --
> Adrian Zubarev
> Sent with Airmail
>
> Am 11. Februar 2017 um 14:08:02, Matthew Johnson (matthew at anandabits.com)
> schrieb:
>
>
>
> 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/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.
>>
>
> 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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170211/5690be56/attachment.html>


More information about the swift-evolution mailing list