[Pitch] consistent public access modifiers


(Matthew Johnson) #1

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.


(Xiaodi Wu) #2

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.

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.

···

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


(Jordan Rose) #3

Hi, Matthew. Thank you for bringing up these issues. I'm going to break my feedback up into separate messages, because I think really the enum and protocol cases are unrelated. Open classes refer to classes that can be subclassed from clients of the current module, and similarly open protocols would be protocols that can be adopted from clients of the current module. Public-but-not-open classes cannot be subclassed from outside the current module, but they can still be subclassed within the module. By contrast, "open" enums can grow new cases in new versions of the library, but clients still can't add cases. (That's not a totally unreasonable feature to ever consider, but it's not the one we need now.)

This message will talk about protocols (and a bit about classes); I'll put my thoughts on enums in another message.

I'm with you (and Adrian) in thinking "public-but-not-open" protocols are useful. The Apple frameworks have a good handful of these, for cases where a framework doesn't want to tie itself to a particular type, but still wants to vend something with certain operations. Public-but-not-open protocols also allow for non-public requirements, i.e. operations that every conforming type has but only the library needs to call.

However, we still have a hurdle here: is this useful enough to change the defaults for it? Another way to implement this is to leave 'public' as is (with its open-like behavior), but have an annotation to say that it can only be conformed to from within the module. (`@closed public protocol Foo`) This isn't necessarily what we would have done from the start, but breaks between Swift 3 and Swift 4 have a higher bar to clear than between Swift 2 and Swift 3.

It's worth noting that this can all be emulated by a struct with a non-public field of protocol type and forwarding operations, but that's a lot of extra work today. We want to make the correct thing easy; in the face of writing a wrapper type, I suspect many library authors would just give up and make the protocol public.

I don't think the proposed "closed" for classes and protocols is interesting. "No new subclasses/adopters" seems only marginally useful for optimization, and not at all useful at a semantic level. "public-but-not-open" is the interesting access level.

That's about all I've got for protocols. Thanks again for bringing it up.
Jordan

P.S. For classes, note that 'final' is essentially a performance optimization at this point. I'm not even sure we should bother displaying it in generated interfaces (although the compiler should still be able to take advantage of it in clients).

···

On Feb 8, 2017, at 15: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


(Xiaodi Wu) #4

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.

···

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


(Karl) #5

A couple of points:

1) Protocols can also be sub-typed as well as conformed to. “Open” for classes refers to sub-typing. As I understand it, you would not be able to derive a protocol from a non-open protocol. Perhaps worth mentioning.

2) Enums are conceptually closed - that’s the whole point behind requiring exhaustive switches. Allowing later library versions to add/remove cases is important, but adding default cases to handle case “?” sounds like a bad solution. With zero information about what is happening, and the chance that some cases may not exist any more, anything you write in such a default statement would be useless and nonsensical.

I think API versioning is the better way to deal with this. Version 3 of MyLibrary.MyEnum has cases { A, B, C }, and Version 3.1 has cases { A, C, D, E }. I should be able to write code which handles either complete set of cases, depending on which version of MyLibrary is installed.

···

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


(David Hart) #6

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.

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.

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


(Brent Royal-Gordon) #7

Sorry, I meant to jump in a lot earlier than this, but lost track of this thread on my mental to-do list. I've read most of the thread, but I really can't keep all the replies in my head at once, so I apologize if some of this is duplicative.

I agree with Jordan Rose that "closed enums" and "closed protocols" are separate things and they should be discussed separately, so I'll be doing that here. But first, a criticism of both:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`.

If the `closed` keyword is to stand alone as an access level, I think `closed` is a bad choice. `open` is acceptable because it sounds as visible or even *more* visible than `public`. (AppKit is "public", but GTK is "open". Which exposes more of itself to a programmer?) But `closed` sounds like some form of privacy. I think that, unless it's paired with a word like `public`, it will not be understood correctly.

Closed enums:

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 thing is, you do *lots* of things with enums other than exhaustively switching on them. One extremely common example is `Error` enums. But even leaving that aside, there are lots of cases where you construct an enum and hand it off to a library to work or interpret it; adding a case won't break these.

Basically, when an enum is an input to a library, adding cases is safe. When it's an output from a library, adding cases is potentially unsafe, unless the code using the enum is designed to permit additional cases.

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.

But classes *do* have a default: closed, i.e., `public`. It is an extremely soft default, and we've made it as easy as possible to switch to `open`, but `public` is absolutely the default—both because `public` is the keyword people will know from other languages, and because `public` is the term used with non-class-related symbols.

And remember, it's the default for a good reason: `public`'s semantic is the more forgiving one. If you make a class `public` when it should be `open`, fixing it is not a breaking change, but the opposite is.

In the case of enums, public-but-nonexhaustive is the more forgiving semantic. If you make something public-but-nonexhaustive when it should be public-but-exhaustive, fixing it is not a breaking change, but the opposite is.

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.

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
  }

As I mentioned in another post, inheriting from an enum is not really a sensible thing to do. It is perhaps possible that we could eventually introduce `open enum`s, which would permit you to add cases in outside extensions. (That might be useful for error enums, but I honestly can't think of many other use cases.) But enums would need to gain new features—like member overrides attached to cases—to make that useful. All in all, I'm not entirely convinced about open enums.

Closed protocols:

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.

I'm definitely on board with having closed protocols. I've seen several use cases where they'd be helpful.

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.

Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

I support this, and I'd like to demonstrate a use case for it. (Here I will use `open` for the current semantics of `public`, and `public` for visible-but-not-conformable.)

Suppose that you're writing a SQLite wrapper and want to support binding parameters. There are a number of types SQLite supports natively:

  public protocol SQLiteValue {
    init(statement: SQLiteStatement, columnAt index: Int) throws
    func bind(to statement: SQLiteStatement, at index: Int) throws
  }
  extension Int: SQLiteValue {
    public init(statement: SQLiteStatement, columnAt index: Int) throws {
      self = sqlite3_column_int(statement.stmt, index)
    }
    public func bind(to statement: SQLiteStatement, at index: Int) throws {
      try throwIfNotOK(
        sqlite3_bind_int64(statement.stmt, index, self)
      )
    }
  }
  extension Double: SQLiteValue {…}
  extension Data: SQLiteValue {…}
  extension String: SQLiteValue {…}
  extension Optional: SQLiteValue where Wrapped: SQLiteValue {…}

But you also want people to be able to conform their own types to a protocol which adapts itself to this:

  open protocol SQLiteValueConvertible: SQLiteValue {
    associatedtype PrimitiveSQLiteValue: SQLiteValue
    
    init(primitiveSQLiteValue: PrimitiveSQLiteValue) throws
    var primitiveSQLiteValue: PrimitiveSQLiteValue { get }
  }
  extension SQLiteValueConvertible {
    public init(statement: SQLiteStatement, columnAt index: Int) throws {
      let primitive = try PrimitiveSQLiteValue(statement: statement, columnAt: index)
      try self.init(primitiveSQLiteValue: primitive)
    }
    public func bind(to statement: SQLiteStatement, at index: Int) throws {
      let value = primitiveSQLiteValue
      try value.bind(to: statement, at: index)
    }
  }
  
  // Usage:
  extension Bool: SQLiteValueConvertible {
    public init(primitiveSQLiteValue: Int) {
      self = primitiveSQLiteValue != 0
    }
    public var primitiveSQLiteValue: Int {
      return self ? 1 : 0
    }
  }

In this case, there is no good reason to make `SQLiteValue` open—all the functions you'd need are encapsulated anyway. You could even make its requirements non-public, since users don't need to use them directly. But `SQLiteValueConvertible` should be open—it has a narrow interface that's easy to conform to, with all the implementation details neatly encapsulated.

I also think you should be able to refine a `closed` protocol from outside the module, but to conform to the sub-protocol, you'd either need to conform to an `open` sub-protocol of the original, or add a retroactive conformance to a type that already conforms. For instance, suppose I want to write a SQLite logging system in a separate module:

  protocol SQLiteLoggable: SQLiteValue {
    var logDescription: String { get }
  }
  
  struct Money: SQLiteLoggable, SQLiteValueConvertible {
    // OK, conforms to SQLiteValue through SQLiteValueConvertible
  }
  
  extension Int: SQLiteLoggable {
    // OK, retroactive conformance on type original module conformed to SQLiteValue
  }
  
  struct ID: SQLiteLoggable {
    // Error: Cannot conform directly to SQLValue from this module
  }

This is a somewhat more niche feature, and could perhaps be delayed.

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.

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

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.

···

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

--
Brent Royal-Gordon
Architechies


(Jordan Rose) #8

Hi, Matthew. Sorry for the delay in sending my second message. As stated before:

I'm going to break my feedback up into separate messages, because I think really the enum and protocol cases are unrelated. Open classes refer to classes that can be subclassed from clients of the current module, and similarly open protocols would be protocols that can be adopted from clients of the current module. Public-but-not-open classes cannot be subclassed from outside the current module, but they can still be subclassed within the module. By contrast, "open" enums can grow new cases in new versions of the library, but clients still can't add cases. (That's not a totally unreasonable feature to ever consider, but it's not the one we need now.)

This message will talk about enums; the one about protocols (and classes) has been sent out already.

Right. So, distinguishing between "enums that have exactly this set of cases forever" and other enums is critically important; as has been noted in both bug reports and on this list, not doing this has a number of problems:

- For imported enums, init(rawValue:) never produces 'nil', because who knows how anyone's using C enums anyway?
- "Exhaustive" switches over imported enums may not have default cases, or check for "private cases" defined outside the original enum declaration
- Adding a new case to a Swift enum in a library breaks any client code that was trying to switch over it.

At the same time, we really like our exhaustive switches, especially over enums we define ourselves. And there's a performance side to this whole thing too; if all cases of an enum are known, it can be passed around much more efficiently than if it might suddenly grow a new case containing a struct with 5000 Strings in it.

I think there's certain behavior that is probably not terribly controversial:

- When enums are imported from Apple frameworks, they should always be "open", except for a few exceptions like NSRectEdge. (It's Apple's job to handle this and get it right, but if we get it wrong with an imported enum there's still the workaround of dropping down to the raw value.)
- When I define Swift enums in the current framework, there's obviously no compatibility issues; we should treat them as "closed".

Everything else falls somewhere in the middle, both for enums defined in Objective-C:

- If I define an Objective-C enum in the current framework, should it be "closed", because there are no compatibility issues, or "open", because there could still be private cases defined in a .m file?
- If there's an Objective-C enum in another framework (that I built locally with Xcode, Carthage, CocoaPods, SwiftPM, etc.), should it be "closed", because there are no binary compatibility issues, or "open", because there may be source compatibility issues? We'd really like adding a new enum case to not be a breaking change even at the source level.
- If there's an Objective-C enum coming in through a bridging header, should it be "closed", because I might have defined it myself, or "open", because it might be non-modular content I've used the bridging header to import?

And in Swift:

- If there's a Swift enum in another framework I built locally, should it be "closed", because there are no binary compatibility issues, or "open", because there may be source compatibility issues? We'd really like adding a new enum case to not be a breaking change even at the source level.

Let's now flip this to the other side of the equation. All this talk of exhaustiveness mostly just affects 'switch' statements, and possibly the behavior of the synthesized 'init(rawValue:)' for imported enums. If an enum is "open", that implies you must have a 'default' in a switch. In previous (in-person) discussions about this feature, it's been pointed out that the code in an otherwise-fully-covered switch is, by definition, unreachable, and therefore untestable. This also isn't a desirable situation to be in, but it's mitigated somewhat by the fact that there probably aren't many "open" enums you should exhaustively switch over anyway. (Think about Apple's frameworks again.)

For people who like exhaustive switches, we thought about adding a new kind of 'default'—let's call it 'unknownCase' just to be able to talk about it. This lets you get warnings when you update to a new SDK, but is even more likely to be untested code.

One last problem: I started at the very top by saying why I think "open vs. non-open classes and protocols" are very different from "open and closed enums". Since we've already claimed "open" as an inheritance-related term, I think that we should not use it to describe enums. The same applies to "closed", since it would be weird for "open" and "closed" to not be opposites. I don't have a better term than "exhaustive vs. non-exhaustive" for now, but it's not really the enum that's "exhaustive". I'm going to keep using the quoted terms "open" and "closed" for now, but I'd like to switch away from this as we design the feature.

I don't have good answers to all of this, but I do want to see us come up with answers, because it's absolutely necessary to be able to add new cases and not have it be a breaking change for binary frameworks, and I think most people would like this to also exist in some form for source frameworks (packages). My starting point would be:

- All enums (NS_ENUMs) imported from Objective-C are "open" unless they are declared "non-open" in some way (some new header annotation, probably).
- All public Swift enums in modules compiled "with resilience" (still to be designed) have the option to be either "open" or "closed". This is the annotation currently known as "closed <http://jrose-apple.github.io/swift-library-evolution/#closed-enums>" in the Library Evolution doc, with "open" being the default, but this could change. This only applies to libraries not distributed with an app, where binary compatibility is a concern.
- All public Swift enums in modules compiled from source have the option to be either "open" or "closed". I'd recommend making these "open" by default, just to keep the barrier of entry low, but I'd be okay with a migration plan towards making it required even for packages and frameworks compiled from source.
- None of this affects non-public enums, so whatever there.

I think that's about all I have. I'll catch up on the discussion since last week soon.

Jordan

···

On Feb 8, 2017, at 15: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


(Dave Abrahams) #9

IMO we are eventually going to need to have tools for analyzing ABI and
API breakage. It's reasonable to think that such tools might be built
into the compiler.

···

on Wed Feb 08 2017, Xiaodi Wu <swift-evolution@swift.org> wrote:

I agree very much with rationalizing access levels, but I'm not sure I like
this proposal for public vs. closed. How would the compiler stop me from
editing my own code if something is closed? The answer must be that it
can't, so I can't see it as a co-equal to open but rather simply a
statement of intention.

--
-Dave


(Adrian Zubarev) #10

Hurray, I cannot wait to get the consistent behavior of open/public protocols. I’m not sure I could follow the idea behind the proposed closed keyboard/access modifier. It almost felt like closed == final public, am I mistaken something here?

Furthermore, I really would love if the community could revisit how open/public really should behave. When open was implemented and I tried it out without reading the proposal first I bumped into things like open init() which felt really odd. I understand the argumentation from the proposal, but it feels wrong and inconsistent to me.

Here’s how I would have imagined open vs. public. IMHO public should really mean, you cannot subclass, conform or override something in module B from module A.

Modified samples from SE–0117:

// This class is not subclassable outside of ModuleA.
public class NonSubclassableParentClass {
    // This method >is not overridable outside of ModuleA.
    public func foo() {}

    // This method is not overridable outside of ModuleA because
    // its class restricts its access level.
    // It is INVALID to declare it as `open`.
    public func bar() {}

    // The behavior of `final` methods remains unchanged.
    public final func baz() {}
}

// This class is subclassable both inside and outside of ModuleA.
open class SubclassableParentClass {
     
    // Designated initializer that is not overridable outside ModuleA
    public init()
     
    // Another designated initializer that is overridable outside ModuleA
    open init(foo: Int)
     
    // This property is not overridable outside of ModuleA.
    public var size : Int

    // This method is not overridable outside of ModuleA.
    public func foo() {}

    // This method is overridable both inside and outside of ModuleA.
    open func bar() {}

    /// The behavior of a `final` method remains unchanged.
    public final func baz() {}
}

/// The behavior of `final` classes remains unchanged.
public final class FinalClass { }
/// ModuleB:

import ModuleA

// This is allowed since the superclass is `open`.
class SubclassB : SubclassableParentClass {
     
    // Iff certain conditions are met, the superclass initializers are inherited.
    // `init` will stay `public` and won't be overridable.
    //
    // If the conditions are not met, then `init` is not inherited. That does not
    // mean that we can create a new designated `init` that matches it's superclass's
    // designated initializer. The behavior should be consistent, like the
    // superclass's function `foo` is reserved and not overridable, so is `init`
    // reserved in this case and not overridable.
     
    // This is allowed since the superclass's initializer is `open`
    override init(foo: Int) {
        super.init(foo: foo)
    }
     
    init(bar: Int) {
        // We could call a super designated initializer from here
        super.init()
        // or
        super.init(foo: bar)
    }
     
    // This is invalid because it overrides a method that is
    // defined outside of the current module but is not `open'.
    override func foo() { }

    // This is allowed since the superclass's method is overridable.
    // It does not need to be marked `open` because it is defined on
    // an `internal` class.
    override func bar() { }
}
required should always match the same scope level as the type in which it’s defined. That means if the class is open, than any of it’s required initializers will be open as well.

···

--
Adrian Zubarev
Sent with Airmail

Am 9. Februar 2017 um 00:49:04, Xiaodi Wu via swift-evolution (swift-evolution@swift.org) schrieb:

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.

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.
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/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
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #11

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

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.

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

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 Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Wed, Feb 8, 2017 at 17: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


(Matthew Johnson) #12

Hi, Matthew. Thank you for bringing up these issues. I'm going to break my feedback up into separate messages, because I think really the enum and protocol cases are unrelated. Open classes refer to classes that can be subclassed from clients of the current module, and similarly open protocols would be protocols that can be adopted from clients of the current module. Public-but-not-open classes cannot be subclassed from outside the current module, but they can still be subclassed within the module. By contrast, "open" enums can grow new cases in new versions of the library, but clients still can't add cases. (That's not a totally unreasonable feature to ever consider, but it's not the one we need now.)

Hi Jordan. Thanks for replying to my post!

I understand the current behavior of `open` and how it would work for protocols. I also understand the vocabulary that has been used in talking about “open” enums thus far. What I am trying to point out is that there are inconsistencies in the vocabulary we’ve been using thus far.

The ideas of “open” and “closed” both talk about who is able to add to the set of cases / subclasses / conforming types. `public` (without annotation) also does this. But we haven’t been using the terms consistently - we use a different meaning depending on which kind of entity we’re talking about.

For example, as you pointed out, `open` currently means both a module *and* its clients can add new subclasses. It doesn’t seem right to use this same terminology to mean the module can add cases to an enum but clients *can’t* add new cases to the enum. I understand that “open” enums in the sense of the current meaning of the `open` keyword are not a feature we need right away. I noted that this is specifically not proposed in my pitch. But if we ever *do* add this feature, `open enum` seems like the right way to spell it (probably just using the existing case syntax in an extension to add cases in the client).

It’s also worth pointing out that `public` currently has three distinct meanings:
* `public` enums *cannot* have a new case added in a future version of the library is without a breaking change
* `public` classes *can* have a new subclass added in a future version of the library without a breaking change, but clients cannot add subclasses
* `public` protocols have the same semantics as `open` classes, allowing clients to add conforming types

If we want to move forward with *no* breaking change of any kind the situation gets worse:
* `open` enums can have new cases added in a future version of the library, but clients cannot add cases
* `closed` protocols behave like `public` classes while using the same terminology we use for enums with a fixed set of public cases (to which future versions of the library may not add to)

This all seems pretty unfortunate to me. It seems somewhat confusing in a totally unnecessary way. Everyone could learn to live with it, but why should we? I think there is a very reasonable way to use this terminology consistently across all kinds of types.

This message will talk about protocols (and a bit about classes); I'll put my thoughts on enums in another message.

I'm with you (and Adrian) in thinking "public-but-not-open" protocols are useful. The Apple frameworks have a good handful of these, for cases where a framework doesn't want to tie itself to a particular type, but still wants to vend something with certain operations. Public-but-not-open protocols also allow for non-public requirements, i.e. operations that every conforming type has but only the library needs to call.

I’m glad to hear this!

However, we still have a hurdle here: is this useful enough to change the defaults for it? Another way to implement this is to leave 'public' as is (with its open-like behavior), but have an annotation to say that it can only be conformed to from within the module. (`@closed public protocol Foo`) This isn't necessarily what we would have done from the start, but breaks between Swift 3 and Swift 4 have a higher bar to clear than between Swift 2 and Swift 3.

This is certainly an option. I feel like there is a pretty strong case for not going this route though:

* Consistent use of terminology is pretty important.
* The decision to not pick a default between `public` and `open` classes was a good one which applies to protocols and enums as well.
* While it is a breaking change, the impact can be minimal with a multi-release rollout.

Many Swift users will run a migrator when upgrading to a new version of Swift. For these users, a simple mechanical migration will keep their code running without warnings through each version transition. Users who don’t use the migrator will receive a warning in one version which becomes an error in the next, giving them plenty of time to update their code without preventing it from compiling and with no semantic impact.

It's worth noting that this can all be emulated by a struct with a non-public field of protocol type and forwarding operations, but that's a lot of extra work today. We want to make the correct thing easy; in the face of writing a wrapper type, I suspect many library authors would just give up and make the protocol public.

I don't think the proposed "closed" for classes and protocols is interesting. "No new subclasses/adopters" seems only marginally useful for optimization, and not at all useful at a semantic level. "public-but-not-open" is the interesting access level.

I agree that there may not be interesting use cases for this. I would be happy to treat them the same way as `open enum`: as something that could potentially be added in the future that we know how to spell if we ever decide to add it. The primary point I am making is that there is a semantic equivalent of closed enums for classes and protocols. Perhaps somebody will find an interesting use case. If we adopt consistent terminology we know how to apply it to these use cases.

That's about all I've got for protocols. Thanks again for bringing it up.
Jordan

P.S. For classes, note that 'final' is essentially a performance optimization at this point. I'm not even sure we should bother displaying it in generated interfaces (although the compiler should still be able to take advantage of it in clients).

`final` can be pretty useful when reasoning about code. It’s more than just a performance optimization. It also represents a compiler proof about our code.

···

On Feb 10, 2017, at 12:52 PM, Jordan Rose <jordan_rose@apple.com> wrote:

On Feb 8, 2017, at 15: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


(Adrian Zubarev) #13

Hello Jordan, having a clear and constructive opinion like yours is much appreciated in this topic.

As a disclaimer, I haven’t followed the original topic about the new open access modifier that much last year. At day one, when Swift 3 was released, I noticed open everywhere in Apples libraries like UIKit. I quickly had a short glance on how it supposed to work, because I literally run into the issue where the open is prohibited on initializers, which felt odd. Then I had a closer look to the proposal and realized it’s by design; but it still feels strange, because public-but-not-open claims to disallow overriding a type member from module A in module B, where open removes that restriction. Later on thinking how neat the differentiation between public and open was I thought, because it’s an ACCESS MODIFIER, it should work with any type we had in swift. Okay it doesn’t work with value types because they don’t have any sub-typing relationship which the client of my module could create - that’s totally fine by me. At the end of that journey I remembered that I had some protocols which I had to make public before Swift 3 because it was an implementation detail. Also it was really interesting to see that non of my protocols were open by default after the migration process. At that point I tried to open these protocols where I thought the client should be able to conform to them, because public should mean public-but-not-open right? And this is how I hit the wall with this.

I felt I had to share my experience on this.

Now that you’ve mentioned an alternative for protocols, such as @closed public. IMHO this would be a really shame if this was the final decision for this issue.

Iff we ever decide to bring sub-typing to value types, that would imply that we’d end up with open class/struct/enum vs. @closed public protocol, or even worse open class vs. @closed public protocols/struct/enum. It is simply saying inconsistent.

Having an exclusive access modifier for classes is simply not worth calling open an access modifier, because from the definitions it should affect all types and their members but not being exclusive to classes, otherwise it should have been open public, just like final public is.

Having an exclusive attribute for protocols, which only works with the public access modifier is nuts. Furthermore that would not only reserve the @closed attribute, but could potentially lead to even more inconsistency if we consider to add something like @closed to enums or elsewhere. In the current scenario @closed public has simply no other meaning than public on classes would have, but public would be open for protocols. o.O It definitely will become an origin of confusion at that point.

I understand that this is somehow a breaking change, but currently open feels simply as a rushed decision from last year. In its current state I think it’s a regression.

P.S.: Please don’t blame me for my horrible English. :wink:

···

--
Adrian Zubarev
Sent with Airmail

Am 10. Februar 2017 um 19:52:23, Jordan Rose via swift-evolution (swift-evolution@swift.org) schrieb:

Hi, Matthew. Thank you for bringing up these issues. I'm going to break my feedback up into separate messages, because I think really the enum and protocol cases are unrelated. Open classes refer to classes that can be subclassed from clients of the current module, and similarly open protocols would be protocols that can be adopted from clients of the current module. Public-but-not-open classes cannot be subclassed from outside the current module, but they can still be subclassed within the module. By contrast, "open" enums can grow new cases in new versions of the library, but clients still can't add cases. (That's not a totally unreasonable feature to ever consider, but it's not the one we need now.)

This message will talk about protocols (and a bit about classes); I'll put my thoughts on enums in another message.

I'm with you (and Adrian) in thinking "public-but-not-open" protocols are useful. The Apple frameworks have a good handful of these, for cases where a framework doesn't want to tie itself to a particular type, but still wants to vend something with certain operations. Public-but-not-open protocols also allow for non-public requirements, i.e. operations that every conforming type has but only the library needs to call.

However, we still have a hurdle here: is this useful enough to change the defaults for it? Another way to implement this is to leave 'public' as is (with its open-like behavior), but have an annotation to say that it can only be conformed to from within the module. (`@closed public protocol Foo`) This isn't necessarily what we would have done from the start, but breaks between Swift 3 and Swift 4 have a higher bar to clear than between Swift 2 and Swift 3.

It's worth noting that this can all be emulated by a struct with a non-public field of protocol type and forwarding operations, but that's a lot of extra work today. We want to make the correct thing easy; in the face of writing a wrapper type, I suspect many library authors would just give up and make the protocol public.

I don't think the proposed "closed" for classes and protocols is interesting. "No new subclasses/adopters" seems only marginally useful for optimization, and not at all useful at a semantic level. "public-but-not-open" is the interesting access level.

That's about all I've got for protocols. Thanks again for bringing it up.
Jordan

P.S. For classes, note that 'final' is essentially a performance optimization at this point. I'm not even sure we should bother displaying it in generated interfaces (although the compiler should still be able to take advantage of it in clients).

On Feb 8, 2017, at 15: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
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #14

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.

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

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!

···

Sent from my iPad

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:


(Adrian Zubarev) #15

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
}

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
}

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
}

// 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”. 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. That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above). If I’m not mistaken, every enum case is known at compile time, 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.

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

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


(Matthew Johnson) #16

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

A couple of points:

1) Protocols can also be sub-typed as well as conformed to. “Open” for classes refers to sub-typing. As I understand it, you would not be able to derive a protocol from a non-open protocol. Perhaps worth mentioning.

On the contrary. Protocol refinement does not have any impact on the contract between a library and its clients while allowing clients to add conforming types does impact the contract.

If I am implementing a library I may wish to expose a protocol rather than concrete types (or maybe in addition to concrete types), while still requiring that my implementation control what types are allowed to conform to that protocol. Jordan mentioned that there are several examples of this design intent already in Apple frameworks.

On the other hand, allowing clients to refine a library protocol and even extend my library’s types to conform to the refined protocol (if the concrete types are visible) has no impact at all on the library. The client will not be able to modify the conformances to the library’s protocol in any way.

I can’t think of any reason a library or its users would ever benefit by restricting clients from refining its protocols. Can you?

2) Enums are conceptually closed - that’s the whole point behind requiring exhaustive switches. Allowing later library versions to add/remove cases is important, but adding default cases to handle case “?” sounds like a bad solution. With zero information about what is happening, and the chance that some cases may not exist any more, anything you write in such a default statement would be useless and nonsensical.

I think API versioning is the better way to deal with this. Version 3 of MyLibrary.MyEnum has cases { A, B, C }, and Version 3.1 has cases { A, C, D, E }. I should be able to write code which handles either complete set of cases, depending on which version of MyLibrary is installed.

There is already a desire and a plan to allow libraries to add cases in a non-breaking way. This is a perfectly reasonable thing to do if your enum is something like a set of mutually exclusive options that clients will pass in to a library method. Client code should generally not be switching over an enum like this anyway. One could argue that this type need not be an enum at all - a struct with static factory methods / properties would provide the equivalent client syntax without allowing them to perform a switch. But there is a long history of using enums for things like this and it makes the implementation cleaner. There isn’t a good reason to disallow it.

On the other hand, if clients are expected to switch over values of an enum type, I agree with you - the library should probably make it a closed enum. Users should not be expected to write a default clause to handle unknown future cases.

···

On Feb 11, 2017, at 7:37 AM, Karl Wagner <razielim@gmail.com> wrote:

On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Karl) #17

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

A couple of points:

1) Protocols can also be sub-typed as well as conformed to. “Open” for classes refers to sub-typing. As I understand it, you would not be able to derive a protocol from a non-open protocol. Perhaps worth mentioning.

Actually now that I read that again I’m not sure. People use marker protocols to group several protocol requirements, and to write extensions on collections of types based on those collections of requirements. For example:

// Module 1

closed protocol DoesSomething {
    func doSomething()
}

struct MyTypeOne: Collection, DoesSomething { /* … */ }
struct MyTypeTwo: Collection, DoesSomething { /* … */ }
enum MyTypeThree: DoesSomething { /* … */ }

// Module 2

// Should this be allowed? Does it inherit the “closed-ness” of DoesSomething?
protocol CollectionWhichDoesSomething: Collection, DoesSomething {
/* Empty, marker protocol */
}

// Since MyType{One, Two} already conform to DoesSomething, this doesn’t introduce a new conformance to the closed protocol.
// Should it be allowed, then?
extension MyTypeOne: CollectionWhichDoesSomething {}
extension MyTypeTwo: CollectionWhichDoesSomething {}

This could probably get better with better existential support, but write now we can’t use or extend a “Collection & DoesSomething” existential. You have to wrap those requirements in a protocol and add conformances to it, and you might need to do that for closed protocols, too. As long as you don’t introduce new conformances to that closed protocol, nothing in Module 1 would break.

···

On 11 Feb 2017, at 14:37, Karl Wagner <karl.swift@springsup.com> wrote:

On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

2) Enums are conceptually closed - that’s the whole point behind requiring exhaustive switches. Allowing later library versions to add/remove cases is important, but adding default cases to handle case “?” sounds like a bad solution. With zero information about what is happening, and the chance that some cases may not exist any more, anything you write in such a default statement would be useless and nonsensical.

I think API versioning is the better way to deal with this. Version 3 of MyLibrary.MyEnum has cases { A, B, C }, and Version 3.1 has cases { A, C, D, E }. I should be able to write code which handles either complete set of cases, depending on which version of MyLibrary is installed.


(Matthew Johnson) #18

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.

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

···

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

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


(Matthew Johnson) #19

Sorry, I meant to jump in a lot earlier than this, but lost track of this thread on my mental to-do list. I've read most of the thread, but I really can't keep all the replies in my head at once, so I apologize if some of this is duplicative.

Hi Brent, no problem! Thanks for taking time to read it and offer feedback.

I agree with Jordan Rose that "closed enums" and "closed protocols" are separate things and they should be discussed separately, so I'll be doing that here. But first, a criticism of both:

I agree that they are separate things. But there is also important semantic overlap. One of the major motivations behind my proposal is that I think we’re ignoring this semantic overlap and therefore being sloppy with our terminology. I think it would be wise to consider carefully whether it is a good idea to continue ignoring the overlap. I think one can make an argument that it ignoring it is a pragmatic choice, but one can also make an argument that it is hand-wavy.

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.

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`.

If the `closed` keyword is to stand alone as an access level, I think `closed` is a bad choice. `open` is acceptable because it sounds as visible or even *more* visible than `public`. (AppKit is "public", but GTK is "open". Which exposes more of itself to a programmer?) But `closed` sounds like some form of privacy. I think that, unless it's paired with a word like `public`, it will not be understood correctly.

This certainly is a fair criticism. `closed` is the term that has been heavily used by the community and is an inverse of `open`, which makes sense because it is in many respects a semantic inverse. That said, I would embrace a healthy round of bike shedding to try and find a better keyword.

Closed enums:

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 thing is, you do *lots* of things with enums other than exhaustively switching on them. One extremely common example is `Error` enums. But even leaving that aside, there are lots of cases where you construct an enum and hand it off to a library to work or interpret it; adding a case won't break these.

Basically, when an enum is an input to a library, adding cases is safe. When it's an output from a library, adding cases is potentially unsafe, unless the code using the enum is designed to permit additional cases.

Yes, I agree with this. I was overreaching a bit in that paragraph. My point is that you can achieve the same client syntax for library inputs using designs that don’t expose cases publicly (i.e. discourage users from writing a switch). Maybe, at least in some cases, that is what a library should do. Saying it *is* a design smell was an exaggeration, but saying it is a contract worthy of close consideration is accurate.

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.

But classes *do* have a default: closed, i.e., `public`. It is an extremely soft default, and we've made it as easy as possible to switch to `open`, but `public` is absolutely the default—both because `public` is the keyword people will know from other languages, and because `public` is the term used with non-class-related symbols.

Ok, I can accept this. With that in mind, I am arguing for the same “soft default” for enums and protocols. This means the same semantics and it also means that adopting a different contract is not penalized syntactically and that an “error of omission" mistake cannot be made.

And remember, it's the default for a good reason: `public`'s semantic is the more forgiving one. If you make a class `public` when it should be `open`, fixing it is not a breaking change, but the opposite is.

Yes, of course. This was the right decision.

In the case of enums, public-but-nonexhaustive is the more forgiving semantic. If you make something public-but-nonexhaustive when it should be public-but-exhaustive, fixing it is not a breaking change, but the opposite is.

Agree.

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
  }

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.

As I mentioned in another post, inheriting from an enum is not really a sensible thing to do. It is perhaps possible that we could eventually introduce `open enum`s, which would permit you to add cases in outside extensions. (That might be useful for error enums, but I honestly can't think of many other use cases.) But enums would need to gain new features—like member overrides attached to cases—to make that useful. All in all, I'm not entirely convinced about open enums.

I agree. `open` enums are a possibly interesting idea, but not necessarily a useful one. That’s why included a note that specifically excluded them from the pitch.

Closed protocols:

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.

I'm definitely on board with having closed protocols. I've seen several use cases where they'd be helpful.

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.

Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

I support this, and I'd like to demonstrate a use case for it. (Here I will use `open` for the current semantics of `public`, and `public` for visible-but-not-conformable.)

Suppose that you're writing a SQLite wrapper and want to support binding parameters. There are a number of types SQLite supports natively:

  public protocol SQLiteValue {
    init(statement: SQLiteStatement, columnAt index: Int) throws
    func bind(to statement: SQLiteStatement, at index: Int) throws
  }
  extension Int: SQLiteValue {
    public init(statement: SQLiteStatement, columnAt index: Int) throws {
      self = sqlite3_column_int(statement.stmt, index)
    }
    public func bind(to statement: SQLiteStatement, at index: Int) throws {
      try throwIfNotOK(
        sqlite3_bind_int64(statement.stmt, index, self)
      )
    }
  }
  extension Double: SQLiteValue {…}
  extension Data: SQLiteValue {…}
  extension String: SQLiteValue {…}
  extension Optional: SQLiteValue where Wrapped: SQLiteValue {…}

But you also want people to be able to conform their own types to a protocol which adapts itself to this:

  open protocol SQLiteValueConvertible: SQLiteValue {
    associatedtype PrimitiveSQLiteValue: SQLiteValue
    
    init(primitiveSQLiteValue: PrimitiveSQLiteValue) throws
    var primitiveSQLiteValue: PrimitiveSQLiteValue { get }
  }
  extension SQLiteValueConvertible {
    public init(statement: SQLiteStatement, columnAt index: Int) throws {
      let primitive = try PrimitiveSQLiteValue(statement: statement, columnAt: index)
      try self.init(primitiveSQLiteValue: primitive)
    }
    public func bind(to statement: SQLiteStatement, at index: Int) throws {
      let value = primitiveSQLiteValue
      try value.bind(to: statement, at: index)
    }
  }
  
  // Usage:
  extension Bool: SQLiteValueConvertible {
    public init(primitiveSQLiteValue: Int) {
      self = primitiveSQLiteValue != 0
    }
    public var primitiveSQLiteValue: Int {
      return self ? 1 : 0
    }
  }

In this case, there is no good reason to make `SQLiteValue` open—all the functions you'd need are encapsulated anyway. You could even make its requirements non-public, since users don't need to use them directly. But `SQLiteValueConvertible` should be open—it has a narrow interface that's easy to conform to, with all the implementation details neatly encapsulated.

I also think you should be able to refine a `closed` protocol from outside the module, but to conform to the sub-protocol, you'd either need to conform to an `open` sub-protocol of the original, or add a retroactive conformance to a type that already conforms.

Yes, I agree with this. I didn’t call out protocol refinement in the initial pitch but it came up earlier in the discussion. Refining closed protocols has no semantic impact on the library itself, including it’s ability to evolve resiliently so there is no reason to prevent users from doing it.

For instance, suppose I want to write a SQLite logging system in a separate module:

  protocol SQLiteLoggable: SQLiteValue {
    var logDescription: String { get }
  }
  
  struct Money: SQLiteLoggable, SQLiteValueConvertible {
    // OK, conforms to SQLiteValue through SQLiteValueConvertible
  }
  
  extension Int: SQLiteLoggable {
    // OK, retroactive conformance on type original module conformed to SQLiteValue
  }
  
  struct ID: SQLiteLoggable {
    // Error: Cannot conform directly to SQLValue from this module
  }

This is a somewhat more niche feature, and could perhaps be delayed.

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.

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.

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.

It sounds like we’re mostly on the same page here, but you have identified an alternative name for what I have called `closed` that might be a better one: `fixed`. It would require taking a little bit different approach to defining the semantics than I have taken with `closed`, but I think it could work in a way that still solves the problem of inconsistency that I have pointed out.

This exactly a reply to you Brent, but I wanted to add to this thread the step-by-step line of reasoning which led to my current pitch in case it might be helpful.

1. We should acknowledge that there is a meaningful semantic overlap between enum cases, subclasses, and protocol conformances.
2. Having acknowledged the overlap, we should strive to keep the language consistent syntactically and semantically where applicable.
3. When we introduced `open` we expanded the scope of access modifiers such that they talk about not just visibility, but also who can subclass a class.
4. Taking 1-3 together, it follows that speaking about who can add cases to an enum or conformance to a protocol is also in scope for access modifiers.
5. `public` currently has a different semantic contract in regards to who can add cases, subclass or conformance (i.e. the relevant semantic overlap).
6. Because we have `open` as a keyword that means *clients* can add subclasses, we should fix the current inconsistency with protocols by making them use the same keywords as classes (after a multi-release transition).
7. We make `public enum` be the resilient variety which would match the semantics of `public class` and `public protocol` (post-6).
8. We need a new way to spell the current behavior we get with `public enum` (i.e. closed).
9. In the `open` discussion we concluded that we don't want a library author to accidentally publish semantics they didn't intend through an "error of omission" (i.e. forgetting an annotation). It would also be good to avoid errors of omission in regards to publishing closed and resilient enums.
10. We also didn't want to subtly favor `open` or `public` by making one syntactically lighter weight than the other. It would also be good to avoid syntactic preference of either closed or resilient enums.
11. We want to avoid using annotations when an as-good (or better) solution is available that does not require them.
12. From the above it follows that the logical thing to do is to introduce an access modifier that specifies that the complete set of cases is public and won't change (module breaking changes). This modifier would speak about cases in a way that is effectively an inverse of what `open` means for classes.
13. Because of the inverse relationship with `open` and because it is a term already in common use, `closed` is the most obvious keyword to use, but bikeshedding is expected, as always.
14. Under this system, `open enum`, `closed class` and `closed protocol` all have a well defined meaning. We won't add them right away, but we could do so in the future if we find interesting use cases.

···

On Feb 13, 2017, at 8:24 AM, Brent Royal-Gordon <brent@architechies.com> wrote:

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

--
Brent Royal-Gordon
Architechies


(Adrian Zubarev) #20

Is that assumption correct?

// Module A
public protocol SQLiteValue {
    init(statement: SQLiteStatement, columnAt index: Int) throws
    func bind(to statement: SQLiteStatement, at index: Int) throws
}

// Module B
protocol SQLiteLoggable : SQLiteValue {
    var logDescription: String { get }
}
I could not follow your example there. If SQLiteLoggable is from module B than this should be an error in my opinion. Otherwise open would have less meaning on protocols, because you always could create an empty protocol that has a super public protocol which you’re not allowed to conform to in module B. This would be a silly workaround to being able to conform to it SQLiteValue indirectly without any further requirement like in SQLiteValueConvertible.

That said, it makes no sense to me to allow that, because it’s simply a workaround to conform to a protocol which public-but-not-open tries to prevent.

// Module X
public protocol A {}

open protocol AA : A { func foo() } // Fine

// Module Y
struct B : A {} // Error
struct B : AA { func foo() { .. } } // Okay

protocol C : A {} // Error because `struct B : C` would equal `struct B : A`
protocol CC : AA {} // Should work even empty, because we have more requirement from `AA`
public should have a consistent meaning across all types from module A in module B, which is ‘not allowed to sub-type from’ and in case of protocols additionally ‘not allowed to conform to’.