Sent from my iPad
> On Feb 11, 2017, at 12:38 PM, Adrian Zubarev via swift-evolution > <[email protected]> wrote: > > That makes actually sense to me if we think of it that we never will get any > sub-typing for enums. I’m still undecided on that sub-typing topic about > value types. Just out of curiosity it would be interesting to hear from the > core team if this would be a future direction for Swift or not, be it in > Swift 20 or whatever. :) > The core team has indicated support for value subtyping. It's unclear when this might happen though. I am working on writing up some ideas related to this right now. I hope that helps you to see how value subtyping fits into the hypothetical Swift with `closed` as an access modifier I am proposing. This will be more of a "manifesto" style document than a proposal. It will lay out the landscape a bunch of related ideas, some of which may have ABI impact (and this be relevant for consideration now) and others will be left for future consideration. > Back on the original topic: If the community feels we need something like > closed, then so be it, I don’t mind having flexibility because it makes the > language more powerful. > We definitely need it for enums. The only question is how we spell it and whether the semantics have a clear and consistent relationship with how we treat classes, protocols and the `open` access modifier. My proposal is that we do it in a way that is clear and consistent. > > > -- > Adrian Zubarev > Sent with Airmail > > Am 11. Februar 2017 um 19:31:10, Xiaodi Wu ([email protected]) schrieb: > >> 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 >>> <[email protected]> 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 >>> ([email protected]) 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 ([email protected]) >>>> schrieb: >>>> >>>>> >>>>> >>>>> Sent from my iPad >>>>> >>>>> On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution >>>>> <[email protected]> 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 >>>>>> ([email protected]) schrieb: >>>>>> >>>>>>> On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution >>>>>>> <[email protected]> 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 >>>>>>> [email protected] >>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution >>>>>> >>>>>> _______________________________________________ >>>>>> swift-evolution mailing list >>>>>> [email protected] >>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution >>>> >>> >>> >>> _______________________________________________ >>> swift-evolution mailing list >>> [email protected] >>> https://lists.swift.org/mailman/listinfo/swift-evolution >>> >> > > _______________________________________________ > swift-evolution mailing list > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
