[Pitch] consistent public access modifiers

Sent from my iPad

>
> 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 }`.

I don't quite follow this. In order for `final` to mean something when applied to a kind of entity it should also be meaningful to non-final entities of that same kind. What would a non-final case be?

···

Sent from my iPad

On Feb 11, 2017, at 12:44 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sat, Feb 11, 2017 at 12:42 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:
> On Feb 11, 2017, at 7:22 AM, Rien <Rien@Balancingrock.nl> wrote:

>
> Regards,
> Rien
>
> Site: http://balancingrock.nl
> Blog: http://swiftrien.blogspot.com
> Github: Balancingrock (Rien) · GitHub
> Project: http://swiftfire.nl
>
>
>
>
>
>> On 11 Feb 2017, at 14:07, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:
>>
>>
>>
>> Sent from my iPad
>>
>>> On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <swift-evolution@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@swift.org) schrieb:
>>>
>>>> On Wed, Feb 8, 2017 at 5:05 PM, 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.
>>>>
>>>> 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@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
>> _______________________________________________
>> 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

Sent from my iPad

Sent from my iPad

>
> 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 }`.

I don't quite follow this. In order for `final` to mean something when
applied to a kind of entity it should also be meaningful to non-final
entities of that same kind. What would a non-final case be?

Yeah, you're right; that doesn't quite cut it.

···

On Sat, Feb 11, 2017 at 12:53 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 11, 2017, at 12:44 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sat, Feb 11, 2017 at 12:42 PM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

> On Feb 11, 2017, at 7:22 AM, Rien <Rien@Balancingrock.nl> wrote:

>
> Regards,
> Rien
>
> Site: http://balancingrock.nl
> Blog: http://swiftrien.blogspot.com
> Github: Balancingrock (Rien) · GitHub
> Project: http://swiftfire.nl
>
>
>
>
>
>> On 11 Feb 2017, at 14:07, Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:
>>
>>
>>
>> Sent from my iPad
>>
>>> On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution < >> swift-evolution@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@swift.org) schrieb:
>>>
>>>> On Wed, Feb 8, 2017 at 5:05 PM, 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.
>>>>
>>>> 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@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
>> _______________________________________________
>> 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

Sent from my iPad

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'm pretty neutral on what kind of source compatibility strategy we would adopt. I am happy to defer to the community and core team.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.

Jordan supported allowing protocols to have the same choice of contract that classes do today. `public protocol` has the same meaning as `open class` today so if we want consistency we need a breaking change.

Sure; I was specifically considering the phased introduction of `closed`. It's been a while since I've thought about how to phase in a change regarding public protocols and open protocols.

That said, others make good points about _conforming to_ protocols by a type vs. _refining_ protocols by another protocol, and whether either or both of these is more akin to subclassing a class.

This is something that was in the back of my mind for months (I’ve thought about this off and on since last summer). My conclusion is that *conforming* is the important relationship, at least in terms of the `open`, and `closed` discussion.

As I mentioned in my reply to Karl, I can’t think of any benefit that would be afforded to either a library or its clients by restricting refinement. Obviously clients get more flexibility if they *can* refine protocols defined by a library. From the perspective of a library author nothing changes if a client refines a protocol it defines. All of the semantics of the code in the library is identical either way, as is it’s options for future evolution.

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

Why do you say "if the enum is in the same project, offer a fix-it to insert `closed`? If the enum is in the same project we can perform an exhaustive switch regardless of its public API contract (except for `open` enums if we decide to add those).

Hmm, well now I'm not in favor of my own suggestion. A public enum, though it may gain or lose cases in future versions, can be exhaustively switched over in the present whether it's same-module or third-party. No warning or error should issue on attempting to switch over a public enum without a default case.

This is true for the current semantics of `public enum`. But what I am suggesting is that this semantic be called `closed enum`. `public enum` would allow libraries to add new cases resiliently. This is the same semantic for `public enum` that is mentioned in the Library Evolution document (which spells my `closed enum` as `@closed public enum`).

We have to require a default case for resilient enums because the client may run against a future version of the library with a new case. I think a couple people have mentioned either allowing an implicit default case with `break` or `fatalError` to be synthesized but I am strongly opposed to this. The only other option is a compiler error for a switch over a resilient enum that does not have a default clause.

···

On Feb 11, 2017, at 12:40 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:
On Feb 10, 2017, at 9:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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.

If the community and core team support this strategy I will also. It seems reasonable and speeds up the transition by using the point release. That's a great idea!

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@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@anandabits.com) schrieb:

Sent from my iPad

On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <swift-evolution@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@swift.org) schrieb:

On Wed, Feb 8, 2017 at 5:05 PM, 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 (swift/LibraryEvolution.rst at main · apple/swift · GitHub) 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@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

Hi Matthew,

I've read your proposal ideas and most of the discussions on the thread, and I'd like to provide some personal feedback.

Swift already has a complicated "access modifier" story so I think we really want a good reason to introduce a new one. And the problem I see is that `closed` has much less semantic weight than the other modifiers.

How so? I’m not sure if I catch your meaning here. It feels to me like it has the same semantic weight as `open`: prohibiting future versions of a module from adding cases / subclasses / conformances is roughly the inverse of lifting the restriction that clients cannot add those things. Therefore it has roughly the same degree of additional meaning over `public` as `open` does.

The difference I see is precisely that 'public' and 'open' modifiers limit what the client of a module can do while closed limits what future versions of a module can do. Feels quite different to me.

This is a reasonable point and is perhaps the strongest argument made against my proposal thus far. However, I think we have to consider my proposal relative to the alternatives.

The only alternative I am aware of is making `public enum` the resilient variety and using `@closed public enum` for the closed variety. This means that `public` will have at least two different semantics (three if we don’t reconcile classes and protocols). It also means that the resilient variety is effectively the default. I am really happy that we decide not to have a default between `open` and `public` and think the best choice is that we don’t have one here either. The fact that we have a way to do this while solving the inconsistent semantics of `public` feels like a net win to me.

···

On Feb 12, 2017, at 10:24 AM, David Hart <david@hartbit.com> wrote:
On 12 Feb 2017, at 16:38, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 12, 2017, at 12:50 AM, David Hart <david@hartbit.com <mailto:david@hartbit.com>> wrote:

First of all, the Library Evolution document you linked says toward at the top that "this document is primarily concerned with binary compatibility, i.e. what changes can safely be made to a library between releases that will not break memory-safety or type-safety, or cause clients to fail to run at all." It seems to me that the @closed introduced in that document is much more about library resilience than about only closing down the addition of new cases: that's why it also talks about reordering and all other changes that can change the memory layout.

Swift 3 having introduced both fileprivate and open has complexified the access level story for developers and library authors. That complexity is the cost that we have paid for more expressiveness. But if we continue adding new access control modifiers to express new semantics, we may be going too far: perfect is the enemy of good.

Both of those arguments explain why I think closed should be introduced, but only as a rarely-used attribute for library authors which need to express ABI resilience, and not as an extra access modifier.

`closed` is about much more than binary compatibility. Any time a library publishes an enum that clients can reasonably be expected to switch statements over the library should strive to make it `closed` wherever possible. Otherwise clients are expected to handle unknown future cases by design. That is a design smell if you ask me. This means that we can expect libraries to often carefully design such enums in a way that allows them to be `closed`. The use case for resilient enums is in things like mutually exclusive option sets received as input to the module and for which it would be unusual for clients of the library to write a switch statement over.

With this in mind, `closed` should not be a rarely-used attribute at all. In fact it will often be the best choice. This is a big motivation behind my desire to see it on equal footing with `public` and `open`.

In regards to the complexity of the access model - if you look closely, `public` has three subtly different meanings today. That kind of inconsistency is part of the complexity of it. And as noted, `closed` is a concept that *will* play a significant role in Swift, regardless of how we spell it. What my proposal aims to do is to incorporate it into a consistent system of outside-the-module access modifiers.

One can make a very reasonable argument that access modifiers should *only* be in the business of talking about visibility and should stay out of the business of talking about “who can add to the set of cases / subclasses / conformances”. The time for that argument was when we had the `open` discussion last year. I happen to like the direction we went because it places `public` and `open` on equal footing. And now that we *have* decided to go in this direction, I think we should stick with it when we introduce `closed`.

David

On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto: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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

Alternative: leave “public enum” as it is now, and spell the resilient version “@resilient enum”

The problem with this approach is that the “default” is the stricter contract and library authors have to remember to add the annotation to opt-out of that stricter contract. The problems created by the stricter contract will only appear later when the author realizes they need to add new cases and now it’s a breaking change.

Responsible library authors should always make an intentional choice, but sometimes even the best of us make mistakes. If a library author makes this mistake it is likely that it won’t be noticed until it is too late. Requiring the library author to make a choice between mutually exclusive options rather than a choice to add or omit an annotation reduces the chance of the library author making this error.

This is the rationale that led to us adding `open` rather than adding something like an `@closed` annotation for classes. The desire to avoid growing lots of annotations in the language was also an important consideration that I believe applies here.

···

On Feb 12, 2017, at 10:39 AM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org> wrote:

Nevin

On Sunday, February 12, 2017, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 12, 2017, at 10:24 AM, David Hart <david@hartbit.com <javascript:_e(%7B%7D,'cvml','david@hartbit.com');>> wrote:

On 12 Feb 2017, at 16:38, Matthew Johnson <matthew@anandabits.com <javascript:_e(%7B%7D,'cvml','matthew@anandabits.com');>> wrote:

On Feb 12, 2017, at 12:50 AM, David Hart <david@hartbit.com <javascript:_e(%7B%7D,'cvml','david@hartbit.com');>> wrote:

Hi Matthew,

I've read your proposal ideas and most of the discussions on the thread, and I'd like to provide some personal feedback.

Swift already has a complicated "access modifier" story so I think we really want a good reason to introduce a new one. And the problem I see is that `closed` has much less semantic weight than the other modifiers.

How so? I’m not sure if I catch your meaning here. It feels to me like it has the same semantic weight as `open`: prohibiting future versions of a module from adding cases / subclasses / conformances is roughly the inverse of lifting the restriction that clients cannot add those things. Therefore it has roughly the same degree of additional meaning over `public` as `open` does.

The difference I see is precisely that 'public' and 'open' modifiers limit what the client of a module can do while closed limits what future versions of a module can do. Feels quite different to me.

This is a reasonable point and is perhaps the strongest argument made against my proposal thus far. However, I think we have to consider my proposal relative to the alternatives.

The only alternative I am aware of is making `public enum` the resilient variety and using `@closed public enum` for the closed variety. This means that `public` will have at least two different semantics (three if we don’t reconcile classes and protocols). It also means that the resilient variety is effectively the default. I am really happy that we decide not to have a default between `open` and `public` and think the best choice is that we don’t have one here either. The fact that we have a way to do this while solving the inconsistent semantics of `public` feels like a net win to me.

First of all, the Library Evolution document you linked says toward at the top that "this document is primarily concerned with binary compatibility, i.e. what changes can safely be made to a library between releases that will not break memory-safety or type-safety, or cause clients to fail to run at all." It seems to me that the @closed introduced in that document is much more about library resilience than about only closing down the addition of new cases: that's why it also talks about reordering and all other changes that can change the memory layout.

Swift 3 having introduced both fileprivate and open has complexified the access level story for developers and library authors. That complexity is the cost that we have paid for more expressiveness. But if we continue adding new access control modifiers to express new semantics, we may be going too far: perfect is the enemy of good.

Both of those arguments explain why I think closed should be introduced, but only as a rarely-used attribute for library authors which need to express ABI resilience, and not as an extra access modifier.

`closed` is about much more than binary compatibility. Any time a library publishes an enum that clients can reasonably be expected to switch statements over the library should strive to make it `closed` wherever possible. Otherwise clients are expected to handle unknown future cases by design. That is a design smell if you ask me. This means that we can expect libraries to often carefully design such enums in a way that allows them to be `closed`. The use case for resilient enums is in things like mutually exclusive option sets received as input to the module and for which it would be unusual for clients of the library to write a switch statement over.

With this in mind, `closed` should not be a rarely-used attribute at all. In fact it will often be the best choice. This is a big motivation behind my desire to see it on equal footing with `public` and `open`.

In regards to the complexity of the access model - if you look closely, `public` has three subtly different meanings today. That kind of inconsistency is part of the complexity of it. And as noted, `closed` is a concept that *will* play a significant role in Swift, regardless of how we spell it. What my proposal aims to do is to incorporate it into a consistent system of outside-the-module access modifiers.

One can make a very reasonable argument that access modifiers should *only* be in the business of talking about visibility and should stay out of the business of talking about “who can add to the set of cases / subclasses / conformances”. The time for that argument was when we had the `open` discussion last year. I happen to like the direction we went because it places `public` and `open` on equal footing. And now that we *have* decided to go in this direction, I think we should stick with it when we introduce `closed`.

David

On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution <swift-evolution@swift.org <javascript:_e(%7B%7D,'cvml','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 <javascript:_e(%7B%7D,'cvml','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

Hi Brent, I understand that SubC.cc as C does not work, that’s the case in my bikeshedding example I noticed that because the enum case is always known at compile time, there will be an error with informs you to downgrade SubC to one of its super-types case. Plus I forget about the RawRepresentable for enums while making this example completely - my bad.

I might not fully understand how sub-typing on value types should work, that’s way I’m looking forward to the document Matthew will write one day. :)

Sub-typing enums to remove cases sounds also really interesting to me. :)

Thank you for your feedback.

···

--
Adrian Zubarev
Sent with Airmail

Am 12. Februar 2017 um 09:57:39, Brent Royal-Gordon (brent@architechies.com) schrieb:

On Feb 11, 2017, at 2:25 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

// Allowed because `C` is open, and open allows sub-typing, conforming
// and overriding to the client
enum SubC : C {
case cc
}

There's a subtle mistake here. For a sum type, subtyping actually means *removing* cases, not *adding* cases. (Or it can mean changing some of the associated types into their subtypes, but that's a different story.)

To understand why, think about casting between `C` and `SubC`. Specifically, imagine this upcast:

SubC.cc as C

What is this supposed to do? There is no `cc` case in `C`, so `SubC.cc` cannot be represented as a `C`. That means `SubC` is not a subtype of `C`. In fact, `SubC` would be a *supertype* of `C`—you would need to say `SubC.cc as? C`, but you could always say `C.c as SubC`.

(The ultimate expression of this is that `Never`—which can be thought of as the subtype of all types—is an empty enum.)

That's not to say that this is a feature we should add; rather, it's to say that the concept of inheritance doesn't really make any sense for `enum`s.

Just something I noticed when I was reading this thread. I'll have more to say on the main topic, hopefully soon.

--
Brent Royal-Gordon
Architechies

Alternative: leave “public enum” as it is now, and spell the resilient
version “@resilient enum”

Nevin

···

On Sunday, February 12, 2017, Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

On Feb 12, 2017, at 10:24 AM, David Hart <david@hartbit.com > <javascript:_e(%7B%7D,'cvml','david@hartbit.com');>> wrote:

On 12 Feb 2017, at 16:38, Matthew Johnson <matthew@anandabits.com > <javascript:_e(%7B%7D,'cvml','matthew@anandabits.com');>> wrote:

On Feb 12, 2017, at 12:50 AM, David Hart <david@hartbit.com > <javascript:_e(%7B%7D,'cvml','david@hartbit.com');>> wrote:

Hi Matthew,

I've read your proposal ideas and most of the discussions on the thread,
and I'd like to provide some personal feedback.

Swift already has a complicated "access modifier" story so I think we
really want a good reason to introduce a new one. And the problem I see is
that `closed` has much less semantic weight than the other modifiers.

How so? I’m not sure if I catch your meaning here. It feels to me like
it has the same semantic weight as `open`: prohibiting future versions of a
module from adding cases / subclasses / conformances is roughly the inverse
of lifting the restriction that clients cannot add those things. Therefore
it has roughly the same degree of additional meaning over `public` as
`open` does.

The difference I see is precisely that 'public' and 'open' modifiers limit
what the client of a module can do while closed limits what future versions
of a module can do. Feels quite different to me.

This is a reasonable point and is perhaps the strongest argument made
against my proposal thus far. However, I think we have to consider my
proposal relative to the alternatives.

The only alternative I am aware of is making `public enum` the resilient
variety and using `@closed public enum` for the closed variety. This means
that `public` will have at least two different semantics (three if we don’t
reconcile classes and protocols). It also means that the resilient variety
is effectively the default. I am really happy that we decide not to have a
default between `open` and `public` and think the best choice is that we
don’t have one here either. The fact that we have a way to do this while
solving the inconsistent semantics of `public` feels like a net win to me.

First of all, the Library Evolution document you linked says toward at the
top that "this document is primarily concerned with binary compatibility,
i.e. what changes can safely be made to a library between releases that
will not break memory-safety or type-safety, or cause clients to fail to
run at all." It seems to me that the @closed introduced in that document is
much more about library resilience than about only closing down the
addition of new cases: that's why it also talks about reordering and all
other changes that can change the memory layout.

Swift 3 having introduced both fileprivate and open has complexified the
access level story for developers and library authors. That complexity is
the cost that we have paid for more expressiveness. But if we continue
adding new access control modifiers to express new semantics, we may be
going too far: perfect is the enemy of good.

Both of those arguments explain why I think closed should be introduced,
but only as a rarely-used attribute for library authors which need to
express ABI resilience, and not as an extra access modifier.

`closed` is about much more than binary compatibility. Any time a library
publishes an enum that clients can reasonably be expected to switch
statements over the library should strive to make it `closed` wherever
possible. Otherwise clients are expected to handle unknown future cases by
design. That is a design smell if you ask me. This means that we can
expect libraries to often carefully design such enums in a way that allows
them to be `closed`. The use case for resilient enums is in things like
mutually exclusive option sets received as input to the module and for
which it would be unusual for clients of the library to write a switch
statement over.

With this in mind, `closed` should not be a rarely-used attribute at all.
In fact it will often be the best choice. This is a big motivation behind
my desire to see it on equal footing with `public` and `open`.

In regards to the complexity of the access model - if you look closely,
`public` has three subtly different meanings today. That kind of
inconsistency is part of the complexity of it. And as noted, `closed` is a
concept that *will* play a significant role in Swift, regardless of how we
spell it. What my proposal aims to do is to incorporate it into a
consistent system of outside-the-module access modifiers.

One can make a very reasonable argument that access modifiers should
*only* be in the business of talking about visibility and should stay out
of the business of talking about “who can add to the set of cases /
subclasses / conformances”. The time for that argument was when we had the
`open` discussion last year. I happen to like the direction we went
because it places `public` and `open` on equal footing. And now that we
*have* decided to go in this direction, I think we should stick with it
when we introduce `closed`.

David

On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution < > swift-evolution@swift.org > <javascript:_e(%7B%7D,'cvml','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 (The swift-evolution Archives
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
<javascript:_e(%7B%7D,'cvml','swift-evolution@swift.org');>
https://lists.swift.org/mailman/listinfo/swift-evolution

Also, note that there will be at least one other similar annotation, but for structs — the evolution document calls it @fixedContents. We want a way to declare that the set of stored properties in a struct will never change, allowing clients to make assumptions about its layout. Unlike @closed enums, @fixedContents structs mostly behave the same. The one important difference is that it will be possible to define designated initializers of @fixedContents structs inside extensions from another module.

Slava

···

On Feb 12, 2017, at 8:49 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 12, 2017, at 10:39 AM, Nevin Brackett-Rozinsky via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Alternative: leave “public enum” as it is now, and spell the resilient version “@resilient enum”

The problem with this approach is that the “default” is the stricter contract and library authors have to remember to add the annotation to opt-out of that stricter contract. The problems created by the stricter contract will only appear later when the author realizes they need to add new cases and now it’s a breaking change.

Responsible library authors should always make an intentional choice, but sometimes even the best of us make mistakes. If a library author makes this mistake it is likely that it won’t be noticed until it is too late. Requiring the library author to make a choice between mutually exclusive options rather than a choice to add or omit an annotation reduces the chance of the library author making this error.

This is the rationale that led to us adding `open` rather than adding something like an `@closed` annotation for classes. The desire to avoid growing lots of annotations in the language was also an important consideration that I believe applies here.

Nevin

On Sunday, February 12, 2017, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 12, 2017, at 10:24 AM, David Hart <david@hartbit.com <>> wrote:

On 12 Feb 2017, at 16:38, Matthew Johnson <matthew@anandabits.com <>> wrote:

On Feb 12, 2017, at 12:50 AM, David Hart <david@hartbit.com <>> wrote:

Hi Matthew,

I've read your proposal ideas and most of the discussions on the thread, and I'd like to provide some personal feedback.

Swift already has a complicated "access modifier" story so I think we really want a good reason to introduce a new one. And the problem I see is that `closed` has much less semantic weight than the other modifiers.

How so? I’m not sure if I catch your meaning here. It feels to me like it has the same semantic weight as `open`: prohibiting future versions of a module from adding cases / subclasses / conformances is roughly the inverse of lifting the restriction that clients cannot add those things. Therefore it has roughly the same degree of additional meaning over `public` as `open` does.

The difference I see is precisely that 'public' and 'open' modifiers limit what the client of a module can do while closed limits what future versions of a module can do. Feels quite different to me.

This is a reasonable point and is perhaps the strongest argument made against my proposal thus far. However, I think we have to consider my proposal relative to the alternatives.

The only alternative I am aware of is making `public enum` the resilient variety and using `@closed public enum` for the closed variety. This means that `public` will have at least two different semantics (three if we don’t reconcile classes and protocols). It also means that the resilient variety is effectively the default. I am really happy that we decide not to have a default between `open` and `public` and think the best choice is that we don’t have one here either. The fact that we have a way to do this while solving the inconsistent semantics of `public` feels like a net win to me.

First of all, the Library Evolution document you linked says toward at the top that "this document is primarily concerned with binary compatibility, i.e. what changes can safely be made to a library between releases that will not break memory-safety or type-safety, or cause clients to fail to run at all." It seems to me that the @closed introduced in that document is much more about library resilience than about only closing down the addition of new cases: that's why it also talks about reordering and all other changes that can change the memory layout.

Swift 3 having introduced both fileprivate and open has complexified the access level story for developers and library authors. That complexity is the cost that we have paid for more expressiveness. But if we continue adding new access control modifiers to express new semantics, we may be going too far: perfect is the enemy of good.

Both of those arguments explain why I think closed should be introduced, but only as a rarely-used attribute for library authors which need to express ABI resilience, and not as an extra access modifier.

`closed` is about much more than binary compatibility. Any time a library publishes an enum that clients can reasonably be expected to switch statements over the library should strive to make it `closed` wherever possible. Otherwise clients are expected to handle unknown future cases by design. That is a design smell if you ask me. This means that we can expect libraries to often carefully design such enums in a way that allows them to be `closed`. The use case for resilient enums is in things like mutually exclusive option sets received as input to the module and for which it would be unusual for clients of the library to write a switch statement over.

With this in mind, `closed` should not be a rarely-used attribute at all. In fact it will often be the best choice. This is a big motivation behind my desire to see it on equal footing with `public` and `open`.

In regards to the complexity of the access model - if you look closely, `public` has three subtly different meanings today. That kind of inconsistency is part of the complexity of it. And as noted, `closed` is a concept that *will* play a significant role in Swift, regardless of how we spell it. What my proposal aims to do is to incorporate it into a consistent system of outside-the-module access modifiers.

One can make a very reasonable argument that access modifiers should *only* be in the business of talking about visibility and should stay out of the business of talking about “who can add to the set of cases / subclasses / conformances”. The time for that argument was when we had the `open` discussion last year. I happen to like the direction we went because it places `public` and `open` on equal footing. And now that we *have* decided to go in this direction, I think we should stick with it when we introduce `closed`.

David

On 9 Feb 2017, at 00: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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

Final has a very different meaning: this type has no subtypes.

···

On Feb 13, 2017, at 10:19 AM, Karl Wagner <razielim@gmail.com> wrote:

As I mentioned earlier, I don't think `closed` is a good keyword standing alone. And I also think that, given that we have `open`, `closed` also won't pair well with `public`—they sound like antonyms when they aren’t.

The semantics I am proposing do have an inverse relationship. That said, it may not be an intuitive or immediately obvious inverse. I am certainly not wedded to the idea of using `closed` as the keyword.

What I instead suggest is that we think of a closed enum as being like a fragile (non-resilient) struct. In both cases, you are committing to a particular design for the type. So I think we should give them both the same keyword—something like:

  @fixed struct Person {
    var name: String
    var birthDate: Date
  }
  @fixed enum Edge {
    case start
    case end
  }

What about “final”?

If you look closely, when most people say “closed enum” they mean a fixed, complete set of cases that are all public. But when people say “closed protocol” they don’t actually mean a fixed, complete set of conformances that are all public. They simply mean clients cannot add conformances. This is the semantic contract of resilient enums, not closed enums.

Yes, our traditional terminology here has been a little bit confused.

What I instead suggest is that we think of a closed enum as being like a fragile (non-resilient) struct. In both cases, you are committing to a particular design for the type. So I think we should give them both the same keyword—something like:

  @fixed struct Person {
    var name: String
    var birthDate: Date
  }
  @fixed enum Edge {
    case start
    case end
  }

You omitted public here. Does that mean you intend for `@fixed` to imply public visibility? If so, I could get behind this. But I am curious why you made it an attribute rather than a keyword.

No, I'm sorry, I meant to say `@fixed public struct` and `@fixed public enum`. I don't think `@fixed` implies public-ness, either, so it would need to be paired with a `public` keyword. There *may* be keywords we could use that would, like `exposed`

I agree that `fixed` (and `closed`) don’t imply `public` in terms of the colloquial meaning of the words and there is a reasonable case that `open` does. I’m not sure I like `exposed`, but maybe it’s possible to find a keyword that would more directly imply `public`.

, but I'm not sure we want to make this feature so prominent,

I have some trouble getting on board with requiring an annotation *in addition* to `public` for the reasons I have already stated, and which led to `open` becoming an access modifier rather than an annotation. It’s possible I could be convinced otherwise, but I think it would require data showing that this really is a rare edge case. If the relatively frequency of closed vs resilient enums is reasonably similar to the relative frequency of public vs open enums I think there is a strong case to make them carry the same syntactic weight, as we did with `open`.

and I'm not sure how that would work with classes you want to both expose and permit subclassing of. (Would that be `exposed open class Foo`?)

Can you elaborate on what you mean by "classes you want to both expose and permit subclassing of”? Do you mean commit to the set of fields being fixed like you indicated with a struct? If so, I’m not sure that is a valuable combination and my instinct is to ban it.

If we did want to support something like that it points to keeping `closed` (as in cases, subclasses and conformances) orthogonal to `fixed` (as in the set of stored properties).

I don't see it mentioned here (maybe I just missed it), but even though we *could* do exhaustiveness checking on non-open protocols, I'm not convinced that's a good idea. Usually when you have several types conforming to a protocol, you should access type-specific behavior through polymorphism, not by switching on the protocol. A protocol is supposed to represent a behavior, not just mark a type in some arbitrary way.

I agree that you should usually be adding polymorphism, but preventing exhaustive switch on what is effectively a style argument seems like an unnecessary restriction to me. There will be times when it could be used to good effect. I think the community has done a pretty good job of figuring out how to use Swift’s many features well and don’t believe it would be frequently abused.

I agree we shouldn't change the language to *prevent* bad style. But this would go beyond that—we'd be putting specific engineering effort solely into *enabling* bad style. At minimum, this should fall so far down our to-do list that we'll probably never get to it.

This assumes that switching over conforming types is bad style. One of the biggest problems with switching over subclasses or conforming types is the fact that you don’t get compiler verification of exhaustiveness. If the language supports exhaustive switching for closed classes and protocols this becomes a non-issue.

I don’t know of any languages that support a kind of type which supports generic and dynamic dispatch as well as exhaustive switch. It may be interesting to have the ability to organize some methods by type (i.e. protocol requirements) and other methods by function (i.e. a protocol extension method with an exhaustive switch).

When you have exhaustive switch these are really just two different ways to organize code. Neither one is inherently better. Each has strengths different strengths. Why not allow the language to support both and let programmers decide which organization of their code is best in a particular case?

I still support this general approach. One spelling could simply be `@nonopen`. Although if we don't use `closed`, we could simply use `@closed` like I suggested—here it really *would* be an antonym to `open`.

I like the idea of using `@nonopen` for the transitional attribute. Both because it “removes the openness” that `public protocol` currently implies. In that sense it is probably the most accurate term we could find and it’s also pretty concise.

It also sounds a little bit awkward, which is normally a reason not to use it, but perhaps that's actually a good thing in a temporary, transitional keyword.

A similar mult-release strategy would work for migrating public enums.

What is it that needs migrating here? Lack of exhaustiveness checking? It sounds like we were planning to break that anyway in some fashion.

Public enums are not currently resilient. Clients are allowed to switch over them without a `default` clause. This means that client code will fail to compile in a version of Swift where `public enum` has the resilient contract unless the library changes to adopt closed semantics or the client adds a default case.

My thinking was that, since most existing `public` enums should probably not be `@fixed`, we should just change the behavior and let some switch statements break. Most `public` protocols, on the other hand, ought to become `open`, so we should flag that change and require an explicit marker like `@nonopen` if you really don't want to change over. But I could be convinced otherwise.

I think this hits on the basis of our disagreement. Is it really the case that *most* existing `public` enums should probably not be `@fixed`? Have you done an analysis to support this? Not just of the standard library, but also Apple’s frameworks and open source modules on Github? We might learn something interesting by doing an analysis like this. It certainly wouldn’t hurt.

If it turns out that closed / fixed is *often* (say 30-40% of the cases or more) the desirable contract using an annotation would result in noisy boilerplate.

On the other hand, if it turns out to be relatively rare (say 5-10%) it would become easier to forget the annotation, making an error of omission. Of course that error can be corrected pretty easily so maybe we it wouldn’t be a big deal if it really is rare.

···

On Feb 14, 2017, at 3:43 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Feb 13, 2017, at 7:45 AM, Matthew Johnson <matthew@anandabits.com> wrote:

--
Brent Royal-Gordon
Architechies

If you look closely, when most people say “closed enum” they mean a fixed, complete set of cases that are all public. But when people say “closed protocol” they don’t actually mean a fixed, complete set of conformances that are all public. They simply mean clients cannot add conformances. This is the semantic contract of resilient enums, not closed enums.

Yes, our traditional terminology here has been a little bit confused.

What I instead suggest is that we think of a closed enum as being like a fragile (non-resilient) struct. In both cases, you are committing to a particular design for the type. So I think we should give them both the same keyword—something like:

  @fixed struct Person {
    var name: String
    var birthDate: Date
  }
  @fixed enum Edge {
    case start
    case end
  }

You omitted public here. Does that mean you intend for `@fixed` to imply public visibility? If so, I could get behind this. But I am curious why you made it an attribute rather than a keyword.

No, I'm sorry, I meant to say `@fixed public struct` and `@fixed public enum`. I don't think `@fixed` implies public-ness, either, so it would need to be paired with a `public` keyword. There *may* be keywords we could use that would, like `exposed`, but I'm not sure we want to make this feature so prominent, and I'm not sure how that would work with classes you want to both expose and permit subclassing of. (Would that be `exposed open class Foo`?)

I thought of a couple of other possible keywords. I had previously mentioned `complete` as an alternative to `closed`. Another option that goes in this direction is `total` - it borrows from the mathematical notion of a total function. I’m not sure how I feel about this option and it still doesn’t necessarily imply “more public than public” very strongly (only in the sense that its totality is known to all which is a rather weak sense).

Another possibility is `transparent`. This does imply “more public than public” a some sense that is similar to `open`. I know `@transparent` (or something like that) has been used as an unsupported(?) attribute hinting to the compiler that a function should be made available for inlining (what is the current status of this?). The meaning of this attribute is in some ways similar to the meaning you ascribe to `fixed`. In all cases, this is only meaningful for `public` entities so it feels like a promising direction.

The one thing that makes me somewhat uncomfortable with this approach is that in the context of structs and functions it has no semantic impact on user code - it is only an optimization, while for enums it wold make a important semantic difference to user code. I’m not sure we should use the same syntax for something that is sometimes an optimization and sometimes is semantically meaningful. This concern is relevant regardless of what the keyword is called.

···

On Feb 14, 2017, at 3:43 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Feb 13, 2017, at 7:45 AM, Matthew Johnson <matthew@anandabits.com> wrote:

I don't see it mentioned here (maybe I just missed it), but even though we *could* do exhaustiveness checking on non-open protocols, I'm not convinced that's a good idea. Usually when you have several types conforming to a protocol, you should access type-specific behavior through polymorphism, not by switching on the protocol. A protocol is supposed to represent a behavior, not just mark a type in some arbitrary way.

I agree that you should usually be adding polymorphism, but preventing exhaustive switch on what is effectively a style argument seems like an unnecessary restriction to me. There will be times when it could be used to good effect. I think the community has done a pretty good job of figuring out how to use Swift’s many features well and don’t believe it would be frequently abused.

I agree we shouldn't change the language to *prevent* bad style. But this would go beyond that—we'd be putting specific engineering effort solely into *enabling* bad style. At minimum, this should fall so far down our to-do list that we'll probably never get to it.

I still support this general approach. One spelling could simply be `@nonopen`. Although if we don't use `closed`, we could simply use `@closed` like I suggested—here it really *would* be an antonym to `open`.

I like the idea of using `@nonopen` for the transitional attribute. Both because it “removes the openness” that `public protocol` currently implies. In that sense it is probably the most accurate term we could find and it’s also pretty concise.

It also sounds a little bit awkward, which is normally a reason not to use it, but perhaps that's actually a good thing in a temporary, transitional keyword.

A similar mult-release strategy would work for migrating public enums.

What is it that needs migrating here? Lack of exhaustiveness checking? It sounds like we were planning to break that anyway in some fashion.

Public enums are not currently resilient. Clients are allowed to switch over them without a `default` clause. This means that client code will fail to compile in a version of Swift where `public enum` has the resilient contract unless the library changes to adopt closed semantics or the client adds a default case.

My thinking was that, since most existing `public` enums should probably not be `@fixed`, we should just change the behavior and let some switch statements break. Most `public` protocols, on the other hand, ought to become `open`, so we should flag that change and require an explicit marker like `@nonopen` if you really don't want to change over. But I could be convinced otherwise.

--
Brent Royal-Gordon
Architechies

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 Fri, Feb 10, 2017 at 9:15 AM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 10, 2017, at 1:06 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Thu, Feb 9, 2017 at 9:57 AM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

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

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@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@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@anandabits.com)
schrieb:

Sent from my iPad

On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution < > swift-evolution@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@swift.org) schrieb:

On Wed, Feb 8, 2017 at 5:05 PM, 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.

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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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.

Yes, this is exactly the point I am making. I am working on writing up some ideas around value subtyping as well. I think (hope) that document will make the similarity of cases and subclasses more clear for those who don't quite see it yet.

···

Sent from my iPad

On Feb 11, 2017, at 12:30 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sat, Feb 11, 2017 at 8:37 AM, Adrian Zubarev via swift-evolution <swift-evolution@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@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@anandabits.com) schrieb:

Sent from my iPad

On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <swift-evolution@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@swift.org) schrieb:

On Wed, Feb 8, 2017 at 5:05 PM, 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.

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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Sent from my iPad

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'm pretty neutral on what kind of source compatibility strategy we would
adopt. I am happy to defer to the community and core team.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback
may or may not have sufficient interestingness.

Jordan supported allowing protocols to have the same choice of contract
that classes do today. `public protocol` has the same meaning as `open
class` today so if we want consistency we need a breaking change.

Sure; I was specifically considering the phased introduction of `closed`.
It's been a while since I've thought about how to phase in a change
regarding public protocols and open protocols.

That said, others make good points about _conforming to_ protocols by a
type vs. _refining_ protocols by another protocol, and whether either or
both of these is more akin to subclassing a class.

This is something that was in the back of my mind for months (I’ve thought
about this off and on since last summer). My conclusion is that
*conforming* is the important relationship, at least in terms of the
`open`, and `closed` discussion.

As I mentioned in my reply to Karl, I can’t think of any benefit that
would be afforded to either a library or its clients by restricting
refinement. Obviously clients get more flexibility if they *can* refine
protocols defined by a library. From the perspective of a library author
nothing changes if a client refines a protocol it defines. All of the
semantics of the code in the library is identical either way, as is it’s
options for future evolution.

Perfect; I'm on board with that rationale.

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

Why do you say "if the enum is in the same project, offer a fix-it to
insert `closed`? If the enum is in the same project we can perform an
exhaustive switch regardless of its public API contract (except for `open`
enums if we decide to add those).

Hmm, well now I'm not in favor of my own suggestion. A public enum, though
it may gain or lose cases in future versions, can be exhaustively switched
over in the present whether it's same-module or third-party. No warning or
error should issue on attempting to switch over a public enum without a
default case.

This is true for the current semantics of `public enum`. But what I am
suggesting is that this semantic be called `closed enum`. `public enum`
would allow libraries to add new cases resiliently. This is the same
semantic for `public enum` that is mentioned in the Library Evolution
document (which spells my `closed enum` as `@closed public enum`).

We have to require a default case for resilient enums because the client
may run against a future version of the library with a new case. I think a
couple people have mentioned either allowing an implicit default case with
`break` or `fatalError` to be synthesized but I am strongly opposed to
this. The only other option is a compiler error for a switch over a
resilient enum that does not have a default clause.

You're quite right. I was clearly thinking more clearly yesterday than
today.

···

On Sat, Feb 11, 2017 at 1:50 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 11, 2017, at 12:40 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 10, 2017, at 9:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:

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.

If the community and core team support this strategy I will also. It
seems reasonable and speeds up the transition by using the point release.
That's a great idea!

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. :)

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.

···

--
Adrian Zubarev
Sent with Airmail

Am 11. Februar 2017 um 19:31:10, Xiaodi Wu (xiaodi.wu@gmail.com) 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 <swift-evolution@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@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@anandabits.com) schrieb:

Sent from my iPad

On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <swift-evolution@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@swift.org) schrieb:

On Wed, Feb 8, 2017 at 5:05 PM, 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 (swift/LibraryEvolution.rst at main · apple/swift · GitHub) 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@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

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Sent from my iPad

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'm pretty neutral on what kind of source compatibility strategy we would adopt. I am happy to defer to the community and core team.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.

Jordan supported allowing protocols to have the same choice of contract that classes do today. `public protocol` has the same meaning as `open class` today so if we want consistency we need a breaking change.

Sure; I was specifically considering the phased introduction of `closed`. It's been a while since I've thought about how to phase in a change regarding public protocols and open protocols.

That said, others make good points about _conforming to_ protocols by a type vs. _refining_ protocols by another protocol, and whether either or both of these is more akin to subclassing a class.

This is something that was in the back of my mind for months (I’ve thought about this off and on since last summer). My conclusion is that *conforming* is the important relationship, at least in terms of the `open`, and `closed` discussion.

As I mentioned in my reply to Karl, I can’t think of any benefit that would be afforded to either a library or its clients by restricting refinement. Obviously clients get more flexibility if they *can* refine protocols defined by a library. From the perspective of a library author nothing changes if a client refines a protocol it defines. All of the semantics of the code in the library is identical either way, as is it’s options for future evolution.

Yeah that’s fine, I was just saying that any proposal should make it clear that those sub-protocols are closed externally (I don’t really like the term “refinements” — those sub-protocols may add new requirements). Perhaps we should require an explicit “closed” annotation when deriving a sub-protocol from a closed protocol in another module?

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

Why do you say "if the enum is in the same project, offer a fix-it to insert `closed`? If the enum is in the same project we can perform an exhaustive switch regardless of its public API contract (except for `open` enums if we decide to add those).

Hmm, well now I'm not in favor of my own suggestion. A public enum, though it may gain or lose cases in future versions, can be exhaustively switched over in the present whether it's same-module or third-party. No warning or error should issue on attempting to switch over a public enum without a default case.

This is true for the current semantics of `public enum`. But what I am suggesting is that this semantic be called `closed enum`. `public enum` would allow libraries to add new cases resiliently. This is the same semantic for `public enum` that is mentioned in the Library Evolution document (which spells my `closed enum` as `@closed public enum`).

We have to require a default case for resilient enums because the client may run against a future version of the library with a new case. I think a couple people have mentioned either allowing an implicit default case with `break` or `fatalError` to be synthesized but I am strongly opposed to this. The only other option is a compiler error for a switch over a resilient enum that does not have a default clause.

The Library Evolution document isn’t gospel; it’s more like a collection of musings and aspirations, so I don’t think we should be bound by the syntax it uses.

WRT your point earlier that enums are not suitable for a mutually-exclusive set of options and that structs should be used instead - I think you have it backwards. That is precisely what enums *are* suitable for. For example, FloatingPointSign and FloatingPointRoundingMode in the standard library. If you want to represent something looser - say an integer which _could_ take any value, but has a few particular salient values, that would best be expressed in a wrapper struct with an embedded enum for the salient values.

I don’t believe that forwards-compatible enums are that vital to the conceptual model that they should be the default, in fact I think they are very conceptually different to how enums are used today and what they were designed for. For example, the compiler squashes the layout of an enum in to the smallest type which can represent all of its cases - so if you only have 2 cases, your enum will be an Int1 (possibly stored in a way which overlaps the payload, if there are spare bits to do so). Making your enum raw-representable as an Int32 or whatever doesn’t actually have any effect on this - an enum of 100 Int32-valued cases will get represented as an Int8; they were not designed to carry arbitrary numbers of additional cases. Conceptually, too, enums are easy to reason about because they can only have one out of a set of known values -- knowing that an objects ’state’ variable is one of { notStarted, running, finished } is what separates an enum from an actual Integer. The truth is there is only so many changes you can make to an enum before you invalidate the reasoning of client code.

As for backwards-compatibility, as I said before, I believe that is best done with versioning. So library authors can add/remove cases, and application developers can follow along while supporting systems that are stuck on older library versions (e.g. Foundation).

If we introduce some kind of forwards-compatible enum, it should explicitly be something different (like an “open” enum). It’s the nichest of niche-cases, would change the layout and prohibit optimisations, and make those objects more difficult to reason about.

···

On 11 Feb 2017, at 20:50, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 11, 2017, at 12:40 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:
On Feb 10, 2017, at 9:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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.

If the community and core team support this strategy I will also. It seems reasonable and speeds up the transition by using the point release. That's a great idea!

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

So, perhaps I'm being simplistic here, but--

At the end of the day, aren't we simply trying to enable a resiliency
feature? Could it not be said that an enum where future added cases aren't
source-breaking is a more resilient enum?

Since there is consensus that the status quo is desirable for a lot of use
cases, couldn't we keep spelling it "public enum" and just spell this
proposed more resilient enum "@resilient public enum"?

···

On Tue, Feb 14, 2017 at 10:09 Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

> On Feb 14, 2017, at 3:43 AM, Brent Royal-Gordon <brent@architechies.com> > wrote:
>
>> On Feb 13, 2017, at 7:45 AM, Matthew Johnson <matthew@anandabits.com> > wrote:
>>
>> If you look closely, when most people say “closed enum” they mean a
fixed, complete set of cases that are all public. But when people say
“closed protocol” they don’t actually mean a fixed, complete set of
conformances that are all public. They simply mean clients cannot add
conformances. This is the semantic contract of resilient enums, not closed
enums.
>
> Yes, our traditional terminology here has been a little bit confused.
>
>>> What I instead suggest is that we think of a closed enum as being like
a fragile (non-resilient) struct. In both cases, you are committing to a
particular design for the type. So I think we should give them both the
same keyword—something like:
>>>
>>> @fixed struct Person {
>>> var name: String
>>> var birthDate: Date
>>> }
>>> @fixed enum Edge {
>>> case start
>>> case end
>>> }
>>>
>>
>> You omitted public here. Does that mean you intend for `@fixed` to
imply public visibility? If so, I could get behind this. But I am curious
why you made it an attribute rather than a keyword.
>
> No, I'm sorry, I meant to say `@fixed public struct` and `@fixed public
enum`. I don't think `@fixed` implies public-ness, either, so it would need
to be paired with a `public` keyword. There *may* be keywords we could use
that would, like `exposed`

I agree that `fixed` (and `closed`) don’t imply `public` in terms of the
colloquial meaning of the words and there is a reasonable case that `open`
does. I’m not sure I like `exposed`, but maybe it’s possible to find a
keyword that would more directly imply `public`.

> , but I'm not sure we want to make this feature so prominent,

I have some trouble getting on board with requiring an annotation *in
addition* to `public` for the reasons I have already stated, and which led
to `open` becoming an access modifier rather than an annotation. It’s
possible I could be convinced otherwise, but I think it would require data
showing that this really is a rare edge case. If the relatively frequency
of closed vs resilient enums is reasonably similar to the relative
frequency of public vs open enums I think there is a strong case to make
them carry the same syntactic weight, as we did with `open`.

> and I'm not sure how that would work with classes you want to both
expose and permit subclassing of. (Would that be `exposed open class Foo`?)

Can you elaborate on what you mean by "classes you want to both expose and
permit subclassing of”? Do you mean commit to the set of fields being
fixed like you indicated with a struct? If so, I’m not sure that is a
valuable combination and my instinct is to ban it.

If we did want to support something like that it points to keeping
`closed` (as in cases, subclasses and conformances) orthogonal to `fixed`
(as in the set of stored properties).

>
>>> I don't see it mentioned here (maybe I just missed it), but even
though we *could* do exhaustiveness checking on non-open protocols, I'm not
convinced that's a good idea. Usually when you have several types
conforming to a protocol, you should access type-specific behavior through
polymorphism, not by switching on the protocol. A protocol is supposed to
represent a behavior, not just mark a type in some arbitrary way.
>>
>> I agree that you should usually be adding polymorphism, but preventing
exhaustive switch on what is effectively a style argument seems like an
unnecessary restriction to me. There will be times when it could be used
to good effect. I think the community has done a pretty good job of
figuring out how to use Swift’s many features well and don’t believe it
would be frequently abused.
>
> I agree we shouldn't change the language to *prevent* bad style. But
this would go beyond that—we'd be putting specific engineering effort
solely into *enabling* bad style. At minimum, this should fall so far down
our to-do list that we'll probably never get to it.

This assumes that switching over conforming types is bad style. One of
the biggest problems with switching over subclasses or conforming types is
the fact that you don’t get compiler verification of exhaustiveness. If
the language supports exhaustive switching for closed classes and protocols
this becomes a non-issue.

I don’t know of any languages that support a kind of type which supports
generic and dynamic dispatch as well as exhaustive switch. It may be
interesting to have the ability to organize some methods by type (i.e.
protocol requirements) and other methods by function (i.e. a protocol
extension method with an exhaustive switch).

When you have exhaustive switch these are really just two different ways
to organize code. Neither one is inherently better. Each has strengths
different strengths. Why not allow the language to support both and let
programmers decide which organization of their code is best in a particular
case?

>
>>> I still support this general approach. One spelling could simply be
`@nonopen`. Although if we don't use `closed`, we could simply use
`@closed` like I suggested—here it really *would* be an antonym to `open`.
>>
>> I like the idea of using `@nonopen` for the transitional attribute.
Both because it “removes the openness” that `public protocol` currently
implies. In that sense it is probably the most accurate term we could find
and it’s also pretty concise.
>
> It also sounds a little bit awkward, which is normally a reason not to
use it, but perhaps that's actually a good thing in a temporary,
transitional keyword.
>
>>>> A similar mult-release strategy would work for migrating public enums.
>>>
>>> What is it that needs migrating here? Lack of exhaustiveness checking?
It sounds like we were planning to break that anyway in some fashion.
>>
>> Public enums are not currently resilient. Clients are allowed to
switch over them without a `default` clause. This means that client code
will fail to compile in a version of Swift where `public enum` has the
resilient contract unless the library changes to adopt closed semantics or
the client adds a default case.
>
> My thinking was that, since most existing `public` enums should probably
not be `@fixed`, we should just change the behavior and let some switch
statements break. Most `public` protocols, on the other hand, ought to
become `open`, so we should flag that change and require an explicit marker
like `@nonopen` if you really don't want to change over. But I could be
convinced otherwise.

I think this hits on the basis of our disagreement. Is it really the case
that *most* existing `public` enums should probably not be `@fixed`? Have
you done an analysis to support this? Not just of the standard library,
but also Apple’s frameworks and open source modules on Github? We might
learn something interesting by doing an analysis like this. It certainly
wouldn’t hurt.

If it turns out that closed / fixed is *often* (say 30-40% of the cases or
more) the desirable contract using an annotation would result in noisy
boilerplate.

On the other hand, if it turns out to be relatively rare (say 5-10%) it
would become easier to forget the annotation, making an error of omission.
Of course that error can be corrected pretty easily so maybe we it wouldn’t
be a big deal if it really is rare.

>
> --
> Brent Royal-Gordon
> Architechies
>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Types have a ton of different implicit/explicit “API”, and access control modifiers often implicitly define that API:

- API for everyone to use when dealing with your concrete type (“public”)
- API for module code when you have related, coupled types that need a higher degree of knowledge (“internal”)
- API for within the implementation of the type itself (“fileprivate”/“private”)
- API to other code to interact with your type in a more general manner (protocol implementation)
- You also have two versions of each of these - instance and static/class-level properties/methods (including initializers)
- Each of these can also have stability aspects - which versions of a framework support the API, whether the API is deprecated or obsoleted, etc. (exposed partially today via “#available")

And classes add even more!:
- Whether subclassing is allowed (‘final’)
- API for subclasses to use for their implementation, but not meant for general usage (typically “protected")
- API which subclasses are allowed to override to implement new logic (“open")
- API which subclasses are forbidden to override because they define business logic used by coupled code ("final"/"closed")
- API which subclasses are required to override (typically “abstract" base classes - Swift and Objective C seem to prefer Delegates instead)

(I’m probably forgetting a few)

So access levels serve three main purposes:
1. to define these API so that a developer interacting with your type knows what is or is not (for instance) a subclass knows what it is or is not allowed to change, code using your types know what is or is not safe to call, etc.
2. to try to enforce these API to be used only by the intend audience
3. to prevent reliance on implementation details as a stable API

Obviously not all of these cases need compiler-enforcement of the API - nor could you have a simple enough system for general purpose consumption which attempted to do so. In the case language features do not document the stakeholders or behavior of the API, regular documentation and processes should attempt to do so.

This IMHO was the majority of the argument against SE-0025 - that if you are already in the same file, you must know the implementation details well enough to know what is or is not safe API. If developers were putting too much code within a single file was a case for a level above fileprivate, not below it. This why I personally pushed to defer until there was a submodule design.

The public/internal/private model is nice because it mirrors code locality, and thus is focused on enforcement of safety. If some other code depends on implementation details of my type that I don’t want to expose to the world, that code is going to be in the same module or even the same file. Hiding implementation details is enforcement for safety.

Classes obviously provide an explosion of complexity in defining behavior because of the additional relationship with sub- and super-classes. I generally push people away from designing their packages to rely on subclassing (instead preferring protocols and aggregation) because keeping this complexity straight and having a good design that reduces coupling is so difficult when dealing with subclassing.

-DW