[Pitch] consistent public access modifiers

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?

The best way to think about `closed` is to just think about `public enum` as it exists today. The contract implies that no new cases will be added because user code is allowed to perform exhaustive switch over the cases. Adding a case is a breaking change.

The insight behind this proposal is that we can also support these semantics with classes and protocols. A `closed` class may have subclasses within the module so long as they are public, but no direct subclasses may be added in future versions (descendants may still be added if the `closed` class has a non-final subclass). Adding a subclass in the future becomes a breaking change.

Similarly with protocols, no new conformances will be added in future versions of the library. As with `closed` classes, if a non-final class conforms to a `closed` protocol subclasses may still be added in the future.

The relationship between `closed` and `final public` is interesting. `final public` is equivalent to `final closed` under this proposal as long as the semantics of `final` implies not only that there are no subclasses *right now*, but also that that no subclasses will ever be added *in the future*. This is the semantics suggested in the Library Evolution document - "final may not be removed from a class or its members. (The presence of final enables optimization.)”. This is a degenerate case of `closed`: new subclasses cannot be added because `final` declares that there will *never be* any subclasses.

···

On Feb 8, 2017, at 7:02 PM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:
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 <mailto: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 <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
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

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

The last explanation is great, now I could follow the idea behind the proposed closed keyword/access modifier. I do now understand the contract on enums, but I’m struggling to understand how closed would work in my own codebase. Assume I had a closed (not the public meaning of your proposal) protocol.

Am I allowed to conform to that protocol inside my library? If it’s a version controlled feature, how to prevent further conformance to that protocol in a newer version?

What would an access modifier like closed mean on type members (final public)? If it’s only a type scope access modifier, it leans towards being an attribute instead.

If it’s not an access modifier, would closed have some versioning paramerters?
Bikeshedding: @closed(1.0.0) (The version, the type was closed)
On current enums: @closed(*) (From the start)
The latter implies in my head that each type needs an explicit version annotation like @available(1.0.0, *)!?

How do we handle bug fixes on closed types? Assume all current enums would get an annotation like @closed(*) public. After normalize enum case representation proposal you might also want to clean up the names of your cases. That means you have to perform a breaking API change.
For example in one of my ‘just for fun’ projects I have two enum cases that look as follows:

case javaScript(String)
case scopedJavaScript(String, scope: Document)
If the mentioned proposal will be accepted I’d like to make these cases shiny.

// v1:
case javaScript(String)
case javaScript(String, scope: Document)
     
// or v2:
case javaScript(String, scope: Document? = nil)

···

--
Adrian Zubarev
Sent with Airmail

Am 9. Februar 2017 um 16:57:40, Matthew Johnson via swift-evolution (swift-evolution@swift.org) schrieb:

On Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com> 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. 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 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 (swift/LibraryEvolution.rst at main · apple/swift · GitHub) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

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

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

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

The most important point IMO is that they *are* co-equal in the sense that
they define a contract between library authors, library users and the
compiler.

Certainly, `open` and your proposed `closed` both represent contracts among
library authors, users, and the compiler. But so do other features that are
not access modifiers--unless your intent is to make all the (not yet
totally finalized) resilience attributes such as `@inlineable` become
access modifiers. That both `open` and the proposed `closed` are both
contracts doesn't make them both access modifiers.

To me, the reason one _could_ justify `open` being spelled like an access
modifier (which I was not supportive of, actually, though I was fine with
`public` not allowing subclassing) is that it quacks like an access
modifier in some ways. In particular, since it offers more "access" to a
class than does `public` by allowing subclassing, one can argue that it
fits at the top of a hierarchy of access levels.

As you define it, `closed` also makes additional guarantees to the end user
than does `public` by (self-)imposing restrictions on the library author.
Thus, it does not fit into a hierarchy of access modifiers where each level
is more "accessible" than the next. Put another way, my point here is that
`closed` is not the opposite of `open` in key ways, as the names might
suggest. In fact, from the perspective of a library user, both `closed` and
`open` would allow you to do more than `public`.

As you note, there are some differences in how the `closed` contract is

supported. But that is far less important than the meaning of the contract
itself.

Since the additional guarantees of both `open` and your proposed `closed`
impose burdens on the library _author_ and offer more flexibility to the
library _user_, I feel it is highly misleading to make them co-equal but
antonyms. They are not the "opposite" of each other and the spelling would
be misleading.

Dave's comment about tools to assist with contract-compatible API evolution

is the right way to think about this. Of course you *can* make breaking
changes, but we want to make it clear when you *are* making a breaking
change, both for source and for ABI compatibility. This will help library
authors, but it also helps users as well as the compiler reason about code
when we are able to offer stronger guarantees.

Yes, this is totally fair.

Most notably, the behavior of public enums *already* has the API contract
of `closed` and we do not want to remove that capability. This proposal
only formalizes how that contract is specified and makes it consistent
across all kinds of types. It *does not* introduce the idea of a closed
semantic contract for a type.

As this change didn't seem in scope for Swift 4 phase 1, I've held off on
discussing my own thoughts on access levels. The idea I was going to
propose in phase 2 was to have simply open and public enums (and
protocols). I really think that completes access levels in a rational way
without introducing another keyword.

The reason I posted now is because formalizing this API contract for enums
must happen before ABI is locked down, and also because there is at least
one protocol in the standard library (`MirrorPath`) which is documented
with the intent that it be `closed`.

I understand the reluctance to introduce another keyword. It isn’t clear
to me what semantics you assign to `open` and `public` enums.

Are you suggesting that they match the semantics defined in my proposal
and suggesting closed enums (i.e. matching the current behavior of `public`
enums) would require an `@closed` annotation as suggested in the Library
Evolution document?

Yes, I am.

I am opposed to this approach because it penalizes the API contract that I
think is often the most appropriate for enums. I strongly prefer that we
adopt the same neutral stance that we when we introduced `open`.

I would not characterize `open` as a neutral stance. But that's neither
here nor there.

The problem (as I see it) with your argument is that, in general, the
following two thoughts are incompatible: (a) the additional burden of a
public API contract should be opt-in; vs. (b) there should be neutrality as
to whether or not one assumes the burden of a particular public API
contract.

Opting in means that one has made the deliberate effort of rejecting some
sort of more natural or default choice. Writing `@closed public enum` can
be easily thought of as opting in, because doing so very clearly requires
actively choosing to add something more than the alternative `public enum`.

Writing `open class` is harder to justify as opting in, because it is not
as obvious that `public class` is some sort of default. (Hence, why I did
not think that `open` should have been an access level, though I was fine
with the proposal otherwise.) The saving grace there is that, in the linear
hierarchy of access levels, `open` is two steps away from the default
access level of `internal`, whereas `public` is only one step removed. So,
one can make the passable rationalization that in choosing `open` one is
escalating from `internal` to `public` to `open`--i.e., that going the
extra step can be regarded as the act of opting in by choosing not to stop
at `public`.

As I see it, one can only be said to opt in to B (vs. an alternative A)
only to the extent that A and B are not neutral choices but rather in some
sort of hierarchy. Again, I'm unconvinced `closed` fits into a linear
hierarchy of access modifiers, and therefore I see spelling `closed` like
`open` as problematic.

On the other hand, you might be suggesting that `public` enums maintain

···

On Thu, Feb 9, 2017 at 9:57 AM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 8, 2017, at 5:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
their current behavior and we simply introduce `open` as a modifier that
reserves the right for the *library* to introduce new cases while
continuing to prohibit *users* from introducing new cases. This approach
has inconsistent semantics for both `public` and `open`. These keywords
would indicate a different API contract for enums than they do for classes
and protocols. In fact, `open` for enums would have a contract analagous
with `public` for classes and protocols. This feels like a recipe for
confusion. IMO, having consistent semantics for each keyword is pretty
important. We already have, and desire to continue to have, three distinct
semantic contracts. If we want keywords with consistent semantics we are
going to have to introduce a new keyword for the third meaning.

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

I’ve been thinking a lot about our public access modifier story lately in
the context of both protocols and enums. I believe we should move further
in the direction we took when introducing the `open` keyword. I have
identified what I think is a promising direction and am interested in
feedback from the community. If community feedback is positive I will
flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow
inheritance of public classes by default or to require an annotation for
classes that could be subclassed outside the module. The decision we
reached was to avoid having a default at all, and instead make `open` an
access modifier. The result is library authors are required to consider
the behavior they wish for each class. Both behaviors are equally
convenient (neither is penalized by requiring an additional boilerplate-y
annotation).

A recent thread (The swift-evolution Archives
Week-of-Mon-20170206/031566.html) discussed a similar tradeoff regarding
whether public enums should commit to a fixed set of cases by default or
not. The current behavior is that they *do* commit to a fixed set of cases
and there is no option (afaik) to modify that behavior. The Library
Evolution document (https://github.com/apple/swift/blob/master/docs/
LibraryEvolution.rst#enums) suggests a desire to change this before
locking down ABI such that public enums *do not* make this commitment by
default, and are required to opt-in to this behavior using an `@closed`
annotation.

In the previous discussion I stated a strong preference that closed enums
*not* be penalized with an additional annotation. This is because I feel
pretty strongly that it is a design smell to: 1) expose cases publicly if
consumers of the API are not expected to switch on them and 2) require
users to handle unknown future cases if they are likely to switch over the
cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same
strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter
regarding whether or not we should allow closed protocols. In a recent
Twitter discussion Joe Groff suggested that we don’t need them because we
should use an enum when there is a fixed set of conforming types. There
are at least two reasons why I still think we *should* add support for
closed protocols.

As noted above (and in the previous thread in more detail), if the set of
types (cases) isn’t intended to be fixed (i.e. the library may add new
types in the future) an enum is likely not a good choice. Using a closed
protocol discourages the user from switching and prevents the user from
adding conformances that are not desired.

Another use case supported by closed protocols is a design where users
are not allowed to conform directly to a protocol, but instead are required
to conform to one of several protocols which refine the closed protocol.
Enums are not a substitute for this use case. The only option is to resort
to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as
clarifying the meaning of `public` and expanding the use of `open`. This
provides consistent capabilities and semantics across enums, classes and
protocols.

`open` is the most permissive modifier. The symbol is visible outside
the module and both users and future versions of the library are allowed to
add new cases, subclasses or conformances. (Note: this proposal does not
introduce user-extensible `open` enums, but provides the syntax that would
be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new
cases, subclasses or conformances. The library reserves the right to add
new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible
publicly with the commitment that future versions of the library are *also*
prohibited from adding new cases, subclasses or conformances.
Additionally, all cases, subclasses or conformances must be visible outside
the module.

Note: the `closed` modifier only applies to *direct* subclasses or
conformances. A subclass of a `closed` class need not be `closed`, in fact
it may be `open` if the design of the library requires that. A class that
conforms to a `closed` protocol also need not be `closed`. It may also be
`open`. Finally, a protocol that refines a `closed` protocol need not be
`closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should
opt-in to all public API contracts without taking a position on what that
contract should be. It does this in a way that offers semantically
consistent choices for API contract across classes, enums and protocols.
The result is that the language allows us to choose the best tool for the
job without restricting the designs we might consider because some kinds of
types are limited with respect to the `open`, `public` and `closed`
semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The
current behavior of enums is equivalent to a `closed` enum under this
proposal and the current behavior of protocols is equivalent to an `open`
protocol under this proposal. Both changes allow for a simple mechanical
migration, but that may not be sufficient given the source compatibility
promise made for Swift 4. We may need to identify a multi-release strategy
for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding
closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning,
possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new
`public` behavior. Brent suggested `@closed`, but as this proposal
distinguishes `public` and `closed` we would need to identify something
else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is
not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new
semantics. `@annotation public protocol` is also allowed, now with a
warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

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

I’m probably better describing things with some bikeshedding code, but feel free to criticize it as much as you’d like.

//===========--------- Module A ---------===========//
@closed public enum A {
    case a
}

extension A {
    case aa // error, because enum is closed
}

This is an error because you can't add cases in an extension. I imagine this is how cases would be added outside the module if we allow `open enum` in the future. But whether or not this is allowed *within* the module is a separate question that is orthogonal to `closed` and `open`.

public func foo(a: A) {
    switch a {
    case .a:
        print("done")
    }
}

public enum B {
    case b
}

extension B {
    case bb // fine, because not-closed enums are extensible
}

As noted above, whether this is allowed or not *within* the module is orthogonal to `closed`. *Outside* the module it would only be possible for enum declared `open` (if we add this feature in the future).

public func bar(b: B) {
    switch b {
    case .b:
        print("b")

    default: // always needed
        print("some other case")
    }
}

// Sub-enum relationships

// Possible even the enum A is closed, because `@closed` only
// closes the extensibility of an enum
enum SubA : A {
    case aa
}

Now you're talking about value subtypes. That is orthogonal. Also, this syntax already has a meaning (the raw value of the enum is A) so we wouldn't be able to use it the way you are intending here. Finally, it is misleading syntax because what you mean here is "A is a subtype of SubA" which is exactly the opposite of what the syntax implies.

All values of A are valid values of SubA, but SubA has values that are not valid values of A.

// The following enum can have a sub-enum in the clients module
open enum C {
    case c
}
public func cool(c: C) {
    switch c {
    case .c:
        print("c")

    default: // always needed
        print("some other case")
    }
}

@closed open enum D {
    case d
}

public func doo(d: D) {
    switch b {
    case .b:
        print("b")
    }
}

// The enum case is always known at any point, no matter
// where the instance comes from, right?

let subA = SubA.aa
let otherSubA = SubA.a // Inherited case

let a: A = subA // error, downgrade the sub-enum to A first
let a: A = otherSubA // okay

foo(a: subA) // error, downgrade the sub-enum to A first
foo(a: otherSubA) // okay

//===========--------- Module B ---------===========//

// Totally fine
switch A.a {
case .a:
    print("done")
}

extension A {
    case aa // not allowed because the enum is closed
}

extension B {
    case bbb
}

switch B.b {
case .b:
    print("b")
default:
    print("somethine else")
}

bar(b: B.bbb) // fine, because the switch statement on enums without
// `@closed` has always`default`

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

let subC = SubC.cc

cool(c: subC) // okay

enum SubD : D {
    case dd
}

doo(d: D.dd)// error, downgrade sub-enum to D first
My point here is, that we should not think of (possible) open enums as enums that the client is allowed to extend. That way we’re only creating another inconsistent case for the open access modifier. As far as I can tell, open as for today means “the client is allowed to subclass/override things from a different module”.

Yes, but subclasses are analogous to enum cases. A subtype of an enum would remove cases. I think you are misunderstanding the relationship of enums to classes and protocols.

And I already said it hundred of times that we should extend this to make open a true access modifier in Swift. That said the meaning of open should become:

The client is allowed to sub-type (currently only classes are supported).
The client is allowed to conform to open protocols
The client is allowed to override open type members
This also means that extensibility is still allowed to public types. Public-but-not-open classes are still extensible today, which is the correct behavior. Extending an enum which is not closed could or probably should be made possible through extensions, because I cannot think of anther elegant way for the client to do so.

This is what `open enum` would allow. It is the proper enum analogue of open classes.

That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above).

Value subtyping is very interesting. I have been working on some ideas around this but I want to keep this thread focused.

If I’m not mistaken, every enum case is known at compile time,

This is true today but will not always be true in the future. That is in large part what this thread is about.

which means to me that we can safely check the case before allowing to assign or pass an instance of a sub-enum to some of its super-enum. (Downgrading an enum case means that you will have to write some code that either mutates your current instance or creates a new one which matches one of the super-enum cases.) Furthermore that allows a clear distinction of what open access modifier does and how @closed behaves.

I'm not going to comment on the rest because it is premised on a misunderstanding of what value subtyping is. I'm going to share some ideas around value subtyping in a new thread as soon as I have a chance to finish putting them together.

···

Sent from my iPad

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

To summarize:

@closed enum - you’re not allowed to add new cases to the enum in your lib + (you’re allowed to create sub-enums)
@closed public enum - you and the client are not allowed to add new cases (+ the client is not allowed to create sub-enums)
@closed open enum - you and the client are not allowed to add new cases (+ the client might create new sub-enums)
enum - you’re allowed to add new cases (default is needed in switch statements) (+ you can create new sub-enums)
public enum - you and the client are allowed to add new cases (+ only you are allowed to create new sub-enums)
open enum - you and the client are allowed to add new cases (everyone can create new sub-enums)
This is a lot of bike shedding of mine, and the idea might not even see any light in Swift at all, but I’d like to share my ideas with the community. Feel free to criticize them or flesh something out into something real. :)

P.S.: If we had something like this:

@closed enum X {
    case x, y
    func foo() {
     switch self {
        case .x, .y:
            print("swift")
    }
}

enum Z : X {
    case z, zz
    override func foo() {
        // Iff `self` is `z` or `zz` then calling super will result in an error.
        // Possible solution: always tell the client to downgrade explicitly the
        // case first if there is an attempt to call super (if mutating),
        // or handle all cases

        switch self {
        case .z, .zz:
            print("custom work")
        default: // or all super-enum cases
            super.foo()
        }
    }
}

--
Adrian Zubarev
Sent with Airmail

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

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

I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums. I believe we should move further in the direction we took when introducing the `open` keyword. I have identified what I think is a promising direction and am interested in feedback from the community. If community feedback is positive I will flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module. The decision we reached was to avoid having a default at all, and instead make `open` an access modifier. The result is library authors are required to consider the behavior they wish for each class. Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html\) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not. The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior. The Library Evolution document (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums\) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the access modifier hierarchy you're proposing and adopt or reject with that clarity, we'll be fine whether we go with `closed` or with `@closed`. But I don't think the source compatibility strategy you list is the most simple or the most easy to understand for end users.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.
- With respect to enums, I don't think we need such a drastic whiplash in terms of what will compile in future versions. Instead, we could take a more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases in switch statements over public enums. Simultaneously, add `closed` or `@closed` (whatever is the approved spelling) and start annotating standard library APIs. The annotation will be purely future-proofing and have no functional effect (i.e. the compiler will do nothing differently for a `closed enum` or `@closed public enum` (as the case may be) versus a plain `public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have a `default` statement: offer a fix-it to insert `default: fatalError()` and, if the enum is in the same project, offer a fix-it to insert `closed` or `@closed`.
3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a switch statement over a public enum doesn't have a `default` statement. Now, new syntax to extend an `open enum` can be introduced and the compiler can treat closed and public enums differently.

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

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

Sent from my iPad

I’ve been thinking a lot about our public access modifier story lately in
the context of both protocols and enums. I believe we should move further
in the direction we took when introducing the `open` keyword. I have
identified what I think is a promising direction and am interested in
feedback from the community. If community feedback is positive I will
flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow
inheritance of public classes by default or to require an annotation for
classes that could be subclassed outside the module. The decision we
reached was to avoid having a default at all, and instead make `open` an
access modifier. The result is library authors are required to consider
the behavior they wish for each class. Both behaviors are equally
convenient (neither is penalized by requiring an additional boilerplate-y
annotation).

A recent thread (https://lists.swift.org/piper
mail/swift-evolution/Week-of-Mon-20170206/031566.html) discussed a
similar tradeoff regarding whether public enums should commit to a fixed
set of cases by default or not. The current behavior is that they *do*
commit to a fixed set of cases and there is no option (afaik) to modify
that behavior. The Library Evolution document (
https://github.com/apple/swift/blob/master/docs/LibraryEvol
ution.rst#enums) suggests a desire to change this before locking down
ABI such that public enums *do not* make this commitment by default, and
are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums
*not* be penalized with an additional annotation. This is because I feel
pretty strongly that it is a design smell to: 1) expose cases publicly if
consumers of the API are not expected to switch on them and 2) require
users to handle unknown future cases if they are likely to switch over the
cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same
strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter
regarding whether or not we should allow closed protocols. In a recent
Twitter discussion Joe Groff suggested that we don’t need them because we
should use an enum when there is a fixed set of conforming types. There
are at least two reasons why I still think we *should* add support for
closed protocols.

As noted above (and in the previous thread in more detail), if the set of
types (cases) isn’t intended to be fixed (i.e. the library may add new
types in the future) an enum is likely not a good choice. Using a closed
protocol discourages the user from switching and prevents the user from
adding conformances that are not desired.

Another use case supported by closed protocols is a design where users
are not allowed to conform directly to a protocol, but instead are required
to conform to one of several protocols which refine the closed protocol.
Enums are not a substitute for this use case. The only option is to resort
to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as
clarifying the meaning of `public` and expanding the use of `open`. This
provides consistent capabilities and semantics across enums, classes and
protocols.

`open` is the most permissive modifier. The symbol is visible outside
the module and both users and future versions of the library are allowed to
add new cases, subclasses or conformances. (Note: this proposal does not
introduce user-extensible `open` enums, but provides the syntax that would
be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new
cases, subclasses or conformances. The library reserves the right to add
new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible
publicly with the commitment that future versions of the library are *also*
prohibited from adding new cases, subclasses or conformances.
Additionally, all cases, subclasses or conformances must be visible outside
the module.

Note: the `closed` modifier only applies to *direct* subclasses or
conformances. A subclass of a `closed` class need not be `closed`, in fact
it may be `open` if the design of the library requires that. A class that
conforms to a `closed` protocol also need not be `closed`. It may also be
`open`. Finally, a protocol that refines a `closed` protocol need not be
`closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should
opt-in to all public API contracts without taking a position on what that
contract should be. It does this in a way that offers semantically
consistent choices for API contract across classes, enums and protocols.
The result is that the language allows us to choose the best tool for the
job without restricting the designs we might consider because some kinds of
types are limited with respect to the `open`, `public` and `closed`
semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The
current behavior of enums is equivalent to a `closed` enum under this
proposal and the current behavior of protocols is equivalent to an `open`
protocol under this proposal. Both changes allow for a simple mechanical
migration, but that may not be sufficient given the source compatibility
promise made for Swift 4. We may need to identify a multi-release strategy
for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding
closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning,
possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new
`public` behavior. Brent suggested `@closed`, but as this proposal
distinguishes `public` and `closed` we would need to identify something
else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is
not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new
semantics. `@annotation public protocol` is also allowed, now with a
warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the
access modifier hierarchy you're proposing and adopt or reject with that
clarity, we'll be fine whether we go with `closed` or with `@closed`. But I
don't think the source compatibility strategy you list is the most simple
or the most easy to understand for end users.

I'm pretty neutral on what kind of source compatibility strategy we would
adopt. I am happy to defer to the community and core team.

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

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

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

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

- With respect to enums, I don't think we need such a drastic whiplash in

terms of what will compile in future versions. Instead, we could take a
more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases
in switch statements over public enums. Simultaneously, add `closed` or
`@closed` (whatever is the approved spelling) and start annotating standard
library APIs. The annotation will be purely future-proofing and have no
functional effect (i.e. the compiler will do nothing differently for a
`closed enum` or `@closed public enum` (as the case may be) versus a plain
`public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have
a `default` statement: offer a fix-it to insert `default: fatalError()`
and, if the enum is in the same project, offer a fix-it to insert `closed`
or `@closed`.

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

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

···

On Sat, Feb 11, 2017 at 6:41 AM, Matthew Johnson <matthew@anandabits.com> wrote:

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

3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a
switch statement over a public enum doesn't have a `default` statement.
Now, new syntax to extend an `open enum` can be introduced and the compiler
can treat closed and public enums differently.

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

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.

Yes, as I stated in my previous reply, there is no reason to disallow this. A library may have good reasons for not allowing clients to add conformances. I can’t think of any good reason why it would want to restrict refinements of its protocols.

···

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

On 11 Feb 2017, at 14:37, Karl Wagner <karl.swift@springsup.com <mailto: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.

I admit that I have not followed all of this discussion, but as far as I see, we could equally well do this by calling “not-open” enum’s “final”.
It seems like a better fit to me.

Regards,
Rien

Site: http://balancingrock.nl
Blog: http://swiftrien.blogspot.com
Github: Balancingrock (Rien) · GitHub
Project: http://swiftfire.nl

···

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

Sent from my iPad

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

I’m probably better describing things with some bikeshedding code, but feel free to criticize it as much as you’d like.

//===========--------- Module A ---------===========//
@closed public enum A {
    case a
}

extension A {
    case aa // error, because enum is closed
}

This is an error because you can't add cases in an extension. I imagine this is how cases would be added outside the module if we allow `open enum` in the future. But whether or not this is allowed *within* the module is a separate question that is orthogonal to `closed` and `open`.

public func foo(a: A) {
    switch a {
    case .a:
        print("done")
    }
}

public enum B {
    case b
}

extension B {
    case bb // fine, because not-closed enums are extensible
}

As noted above, whether this is allowed or not *within* the module is orthogonal to `closed`. *Outside* the module it would only be possible for enum declared `open` (if we add this feature in the future).

public func bar(b: B) {
    switch b {
    case .b:
        print("b")

    default: // always needed
        print("some other case")
    }
}

// Sub-enum relationships

// Possible even the enum A is closed, because `@closed` only
// closes the extensibility of an enum
enum SubA : A {
    case aa
}

Now you're talking about value subtypes. That is orthogonal. Also, this syntax already has a meaning (the raw value of the enum is A) so we wouldn't be able to use it the way you are intending here. Finally, it is misleading syntax because what you mean here is "A is a subtype of SubA" which is exactly the opposite of what the syntax implies.

All values of A are valid values of SubA, but SubA has values that are not valid values of A.

// The following enum can have a sub-enum in the clients module
open enum C {
    case c
}

public func cool(c: C) {
    switch c {
    case .c:
        print("c")

    default: // always needed
        print("some other case")
    }
}

@closed open enum D {
    case d
}

public func doo(d: D) {
    switch b {
    case .b:
        print("b")
    }
}

// The enum case is always known at any point, no matter
// where the instance comes from, right?

let subA = SubA.aa
let otherSubA = SubA.a // Inherited case

let a: A = subA // error, downgrade the sub-enum to A first
let a: A = otherSubA // okay

foo(a: subA) // error, downgrade the sub-enum to A first
foo(a: otherSubA) // okay

//===========--------- Module B ---------===========//

// Totally fine
switch A.a {
case .a:
    print("done")
}

extension A {
    case aa // not allowed because the enum is closed
}

extension B {
    case bbb
}

switch B.b {
case .b:
    print("b")
default:
    print("somethine else")
}

bar(b: B.bbb) // fine, because the switch statement on enums without
// `@closed` has always`default`

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

let subC =
SubC.cc

cool(c: subC) // okay

enum SubD : D {
    case dd
}

doo(d: D.dd)// error, downgrade sub-enum to D first

My point here is, that we should not think of (possible) open enums as enums that the client is allowed to extend. That way we’re only creating another inconsistent case for the open access modifier. As far as I can tell, open as for today means “the client is allowed to subclass/override things from a different module”.

Yes, but subclasses are analogous to enum cases. A subtype of an enum would remove cases. I think you are misunderstanding the relationship of enums to classes and protocols.

And I already said it hundred of times that we should extend this to make open a true access modifier in Swift. That said the meaning of open should become:

  • The client is allowed to sub-type (currently only classes are supported).
  • The client is allowed to conform to open protocols
  • The client is allowed to override open type members
This also means that extensibility is still allowed to public types. Public-but-not-open classes are still extensible today, which is the correct behavior. Extending an enum which is not closed could or probably should be made possible through extensions, because I cannot think of anther elegant way for the client to do so.

This is what `open enum` would allow. It is the proper enum analogue of open classes.

That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above).

Value subtyping is very interesting. I have been working on some ideas around this but I want to keep this thread focused.

If I’m not mistaken, every enum case is known at compile time,

This is true today but will not always be true in the future. That is in large part what this thread is about.

which means to me that we can safely check the case before allowing to assign or pass an instance of a sub-enum to some of its super-enum. (Downgrading an enum case means that you will have to write some code that either mutates your current instance or creates a new one which matches one of the super-enum cases.) Furthermore that allows a clear distinction of what open access modifier does and how @closed behaves.

I'm not going to comment on the rest because it is premised on a misunderstanding of what value subtyping is. I'm going to share some ideas around value subtyping in a new thread as soon as I have a chance to finish putting them together.

To summarize:

  • @closed enum - you’re not allowed to add new cases to the enum in your lib + (you’re allowed to create sub-enums)
  • @closed public enum - you and the client are not allowed to add new cases (+ the client is not allowed to create sub-enums)
  • @closed open enum - you and the client are not allowed to add new cases (+ the client might create new sub-enums)
  • enum - you’re allowed to add new cases (default is needed in switch statements) (+ you can create new sub-enums)
  • public enum - you and the client are allowed to add new cases (+ only you are allowed to create new sub-enums)
  • open enum - you and the client are allowed to add new cases (everyone can create new sub-enums)
This is a lot of bike shedding of mine, and the idea might not even see any light in Swift at all, but I’d like to share my ideas with the community. Feel free to criticize them or flesh something out into something real. :)

P.S.: If we had something like this:

@closed enum X {
    case x, y
    func foo() {
     switch self {
        case .x, .y:
            print("swift")
    }
}

enum Z : X {
    case z, zz
    override func foo() {
        // Iff `self` is `z` or `zz` then calling super will result in an error.
        // Possible solution: always tell the client to downgrade explicitly the
        // case first if there is an attempt to call super (if mutating),
        // or handle all cases

        switch self {
        case .z, .zz:
            print("custom work")
        default: // or all super-enum cases
            super.foo()
        }
    }
}

--
Adrian Zubarev
Sent with Airmail

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

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:
I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums. I believe we should move further in the direction we took when introducing the `open` keyword. I have identified what I think is a promising direction and am interested in feedback from the community. If community feedback is positive I will flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module. The decision we reached was to avoid having a default at all, and instead make `open` an access modifier. The result is library authors are required to consider the behavior they wish for each class. Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html\) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not. The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior. The Library Evolution document (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums\) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the access modifier hierarchy you're proposing and adopt or reject with that clarity, we'll be fine whether we go with `closed` or with `@closed`. But I don't think the source compatibility strategy you list is the most simple or the most easy to understand for end users.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.
- With respect to enums, I don't think we need such a drastic whiplash in terms of what will compile in future versions. Instead, we could take a more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases in switch statements over public enums. Simultaneously, add `closed` or `@closed` (whatever is the approved spelling) and start annotating standard library APIs. The annotation will be purely future-proofing and have no functional effect (i.e. the compiler will do nothing differently for a `closed enum` or `@closed public enum` (as the case may be) versus a plain `public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have a `default` statement: offer a fix-it to insert `default: fatalError()` and, if the enum is in the same project, offer a fix-it to insert `closed` or `@closed`.
3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a switch statement over a public enum doesn't have a `default` statement. Now, new syntax to extend an `open enum` can be introduced and the compiler can treat closed and public enums differently.

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

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

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

It wasn’t my intention to drive to far way off topic with this. The major point of my last bike shedding was that I have to disagree with you about the potential future open enum vs. public enum and closed enum.

public today does not add any guarantee to prevent the client from extending your type. For instance:

// Module A
public class A { public init() {} }

// Module B
extension A {
     
    convenience init(foo: Int) {
        print(foo)
        self.init()
    }
}
That also implies to me that open as an access modifier does not prevent extensibility.

Speaking of opened enums, we really do not mean open enum to allow extensibility where closed enum would mean the opposite. closed or @closed by all the definitions I’ve read so far is what the current public means for enums. If this is going to be fixed to closed enum (@closed public enum) than what we’re currently speaking of is nothing else than public enum, which would be extensible if that future might be added to swift one day.

Again, I see open only as a contract to prevent sub-typing, conformances and overriding, where extensibility of a type a story of it’s own.

Quickly compared to protocols: public-but-not-open protocol from module A should remain extensible in module B. Consistently that would mean that public enum is the enum when we’re talking about future extensibility of that enum from the clients side outside your module. You simply should be able to add new cases directly to your enum if it’s not annotated as closed. open enum on the other hand makes only sense when we’d speak about sub-typing on enums or value types in general.

···

--
Adrian Zubarev
Sent with Airmail

Am 11. Februar 2017 um 14:08:02, Matthew Johnson (matthew@anandabits.com) schrieb:

Sent from my iPad

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

I’m probably better describing things with some bikeshedding code, but feel free to criticize it as much as you’d like.

//===========--------- Module A ---------===========//
@closed public enum A {
    case a
}

extension A {
    case aa // error, because enum is closed
}
This is an error because you can't add cases in an extension. I imagine this is how cases would be added outside the module if we allow `open enum` in the future. But whether or not this is allowed *within* the module is a separate question that is orthogonal to `closed` and `open`.

public func foo(a: A) {
    switch a {
    case .a:
        print("done")
    }
}

public enum B {
    case b
}

extension B {
    case bb // fine, because not-closed enums are extensible
}
As noted above, whether this is allowed or not *within* the module is orthogonal to `closed`. *Outside* the module it would only be possible for enum declared `open` (if we add this feature in the future).

public func bar(b: B) {
    switch b {
    case .b:
        print("b")

    default: // always needed
        print("some other case")
    }
}

// Sub-enum relationships

// Possible even the enum A is closed, because `@closed` only
// closes the extensibility of an enum
enum SubA : A {
    case aa
}

Now you're talking about value subtypes. That is orthogonal. Also, this syntax already has a meaning (the raw value of the enum is A) so we wouldn't be able to use it the way you are intending here. Finally, it is misleading syntax because what you mean here is "A is a subtype of SubA" which is exactly the opposite of what the syntax implies.

All values of A are valid values of SubA, but SubA has values that are not valid values of A.

// The following enum can have a sub-enum in the clients module
open enum C {
    case c
}
public func cool(c: C) {
    switch c {
    case .c:
        print("c")

    default: // always needed
        print("some other case")
    }
}

@closed open enum D {
    case d
}

public func doo(d: D) {
    switch b {
    case .b:
        print("b")
    }
}

// The enum case is always known at any point, no matter
// where the instance comes from, right?

let subA = SubA.aa
let otherSubA = SubA.a // Inherited case

let a: A = subA // error, downgrade the sub-enum to A first
let a: A = otherSubA // okay

foo(a: subA) // error, downgrade the sub-enum to A first
foo(a: otherSubA) // okay

//===========--------- Module B ---------===========//

// Totally fine
switch A.a {
case .a:
    print("done")
}

extension A {
    case aa // not allowed because the enum is closed
}

extension B {
    case bbb
}

switch B.b {
case .b:
    print("b")
default:
    print("somethine else")
}

bar(b: B.bbb) // fine, because the switch statement on enums without
// `@closed` has always`default`

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

let subC = SubC.cc

cool(c: subC) // okay

enum SubD : D {
    case dd
}

doo(d: D.dd)// error, downgrade sub-enum to D first
My point here is, that we should not think of (possible) open enums as enums that the client is allowed to extend. That way we’re only creating another inconsistent case for the open access modifier. As far as I can tell, open as for today means “the client is allowed to subclass/override things from a different module”.

Yes, but subclasses are analogous to enum cases. A subtype of an enum would remove cases. I think you are misunderstanding the relationship of enums to classes and protocols.

And I already said it hundred of times that we should extend this to make open a true access modifier in Swift. That said the meaning of open should become:

The client is allowed to sub-type (currently only classes are supported).
The client is allowed to conform to open protocols
The client is allowed to override open type members
This also means that extensibility is still allowed to public types. Public-but-not-open classes are still extensible today, which is the correct behavior. Extending an enum which is not closed could or probably should be made possible through extensions, because I cannot think of anther elegant way for the client to do so.

This is what `open enum` would allow. It is the proper enum analogue of open classes.

That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above).

Value subtyping is very interesting. I have been working on some ideas around this but I want to keep this thread focused.

If I’m not mistaken, every enum case is known at compile time,

This is true today but will not always be true in the future. That is in large part what this thread is about.

which means to me that we can safely check the case before allowing to assign or pass an instance of a sub-enum to some of its super-enum. (Downgrading an enum case means that you will have to write some code that either mutates your current instance or creates a new one which matches one of the super-enum cases.) Furthermore that allows a clear distinction of what open access modifier does and how @closed behaves.

I'm not going to comment on the rest because it is premised on a misunderstanding of what value subtyping is. I'm going to share some ideas around value subtyping in a new thread as soon as I have a chance to finish putting them together.

To summarize:

@closed enum - you’re not allowed to add new cases to the enum in your lib + (you’re allowed to create sub-enums)
@closed public enum - you and the client are not allowed to add new cases (+ the client is not allowed to create sub-enums)
@closed open enum - you and the client are not allowed to add new cases (+ the client might create new sub-enums)
enum - you’re allowed to add new cases (default is needed in switch statements) (+ you can create new sub-enums)
public enum - you and the client are allowed to add new cases (+ only you are allowed to create new sub-enums)
open enum - you and the client are allowed to add new cases (everyone can create new sub-enums)
This is a lot of bike shedding of mine, and the idea might not even see any light in Swift at all, but I’d like to share my ideas with the community. Feel free to criticize them or flesh something out into something real. :)

P.S.: If we had something like this:

@closed enum X {
    case x, y
    func foo() {
     switch self {
        case .x, .y:
            print("swift")
    }
}

enum Z : X {
    case z, zz
    override func foo() {
        // Iff `self` is `z` or `zz` then calling super will result in an error.
        // Possible solution: always tell the client to downgrade explicitly the
        // case first if there is an attempt to call super (if mutating),
        // or handle all cases

        switch self {
        case .z, .zz:
            print("custom work")
        default: // or all super-enum cases
            super.foo()
        }
    }
}

--
Adrian Zubarev
Sent with Airmail

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

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:
I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums. I believe we should move further in the direction we took when introducing the `open` keyword. I have identified what I think is a promising direction and am interested in feedback from the community. If community feedback is positive I will flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module. The decision we reached was to avoid having a default at all, and instead make `open` an access modifier. The result is library authors are required to consider the behavior they wish for each class. Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html\) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not. The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior. The Library Evolution document (swift/LibraryEvolution.rst at main · apple/swift · GitHub) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the access modifier hierarchy you're proposing and adopt or reject with that clarity, we'll be fine whether we go with `closed` or with `@closed`. But I don't think the source compatibility strategy you list is the most simple or the most easy to understand for end users.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.
- With respect to enums, I don't think we need such a drastic whiplash in terms of what will compile in future versions. Instead, we could take a more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases in switch statements over public enums. Simultaneously, add `closed` or `@closed` (whatever is the approved spelling) and start annotating standard library APIs. The annotation will be purely future-proofing and have no functional effect (i.e. the compiler will do nothing differently for a `closed enum` or `@closed public enum` (as the case may be) versus a plain `public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have a `default` statement: offer a fix-it to insert `default: fatalError()` and, if the enum is in the same project, offer a fix-it to insert `closed` or `@closed`.
3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a switch statement over a public enum doesn't have a `default` statement. Now, new syntax to extend an `open enum` can be introduced and the compiler can treat closed and public enums differently.

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

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

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

  SubC.cc as C

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

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

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

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

···

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

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

--
Brent Royal-Gordon
Architechies

Hi Matthew,

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

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

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

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

···

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

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

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

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

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

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

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

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

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

David

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

I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums. I believe we should move further in the direction we took when introducing the `open` keyword. I have identified what I think is a promising direction and am interested in feedback from the community. If community feedback is positive I will flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module. The decision we reached was to avoid having a default at all, and instead make `open` an access modifier. The result is library authors are required to consider the behavior they wish for each class. Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html\) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not. The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior. The Library Evolution document (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums\) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

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

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.

Yes, Brent was using `public protocol` with the same semantics that `public class` has today (i.e. not open) so this would be an error.

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

No, `struct B : AA` is an error because in order to conform to `AA`, `B` must also conform to `A`, which it cannot because `A` is closed.

protocol C : A {} // Error because `struct B : C` would equal `struct B : A`

No, this is allowed. However the only types that can conform to `C` are types declared in module X that *already* conform to `A`. They may be extended to retroactively conform to `C`.

···

On Feb 13, 2017, at 10:14 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

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

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

The idea is that a type in Module B could not conform to `SQLiteLoggable` without also having a valid conformance to `SQLiteValue`—either by taking a type Module A conforms to `SQLValue` and retroactively conforming it to `SQLiteLoggable`, or by adding a conformance to `SQLiteConvertible` in Module B. So this would not be allow you to work around `SQLiteValue`'s non-open-ness.

···

On Feb 13, 2017, at 8:14 AM, Adrian Zubarev <adrian.zubarev@devandartist.com> wrote:

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.

--
Brent Royal-Gordon
Architechies

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

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

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

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

What about “final”?

Okay thanks, that makes now sense to me. So even if we refine public protocols from module A with a custom protocol in module B, there is no way we can conform any other type from module B to it, but we could conform existing types of module A to our refined protocols.

That’s fine by me, because the consistent restriction of public would remain the same. :)

···

--
Adrian Zubarev
Sent with Airmail

Am 13. Februar 2017 um 17:27:36, Matthew Johnson (matthew@anandabits.com) schrieb:

On Feb 13, 2017, at 10:14 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

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.

Yes, Brent was using `public protocol` with the same semantics that `public class` has today (i.e. not open) so this would be an error.

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
No, `struct B : AA` is an error because in order to conform to `AA`, `B` must also conform to `A`, which it cannot because `A` is closed.

protocol C : A {} // Error because `struct B : C` would equal `struct B : A`
No, this is allowed. However the only types that can conform to `C` are types declared in module X that *already* conform to `A`. They may be extended to retroactively conform to `C`.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

···

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

--
Brent Royal-Gordon
Architechies

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

* public structs *can* have new fields added
* public functions *can* add new parameters, as long as they have defaults, if you are careful about it
* public types *can* have new subtypes added
* public types *can* have new conformances added, although today that will break people and we need to do something about that (on another thread)

I think this is just a false equivalence. Adding cases to an enum within a module is fundamentally different from adding subclasses to a class outside the module. We should not be trying to jam enums into the same keyword that classes use just because I happened to use that term when writing the Library Evolution doc, even if it is a reasonable term for the thing.

(Matthew was very quick about replying to my first message and I was very slow with the second, and I think I already made this point better there. But for cross-referencing purposes it made sense to mention it here too.)

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.

Ah, sorry, yes: 'final' is totally useful within a module to have the property enforced. It's just not super interesting outside the module. (It doesn't even mean there are no subclasses in all cases; features like KVO use a dynamic subclass in their implementation.)

Jordan

···

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

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

The last explanation is great, now I could follow the idea behind the proposed closed keyword/access modifier. I do now understand the contract on enums, but I’m struggling to understand how closed would work in my own codebase. Assume I had a closed (not the public meaning of your proposal) protocol.

Am I allowed to conform to that protocol inside my library? If it’s a version controlled feature, how to prevent further conformance to that protocol in a newer version?

Yes, you are allowed to have as many conforming types inside the library as you wish, as long as the conformances are introduced alongside the protocol. New conformances may not be added in future versions.

I don’t know exactly what the enforcement mechanism would look like, but I imagine it would be similar or identical to the mechanism used for `closed` enums. Maybe someone from the core team can elaborate on what this will look like.

What would an access modifier like closed mean on type members (final public)? If it’s only a type scope access modifier, it leans towards being an attribute instead.

I hadn’t considered this yet but it’s a good question. I don’t have time to consider it deeply, but I think the implication for members is that all overrides are public and no new overrides will be added in the future. It isn’t likely to be useful in this case, but I think we could provide a clear definition.

If it’s not an access modifier, would closed have some versioning paramerters?
Bikeshedding: @closed(1.0.0) (The version, the type was closed)
On current enums: @closed(*) (From the start)

Good question. This is what the Library Evolution document has to say:

@closed is a versioned attribute. This is so that clients can deploy against older versions of the library, which may have non-public cases in the enum. (In this case the client must manipulate the enum as if the @closed attribute were absent.) All cases that are not versioned become implicitly versioned with this number.

I would prefer to see `closed` be an access modifier so it isn’t penalized with verbosity, particularly for enums. I think the best approach is that `closed` with no version specifier is allowed if it is included in the initial release of a module. `public` enums may be changed to `closed(n.m.x)` in future versions of a module. There is already precedent for parameterized access modifiers: `private(set)`.

The latter implies in my head that each type needs an explicit version annotation like @available(1.0.0, *)!?

No, I don’t think so.

How do we handle bug fixes on closed types? Assume all current enums would get an annotation like @closed(*) public. After normalize enum case representation proposal <https://github.com/dduan/swift-evolution/blob/185b12711bfe1e3b7982d255b538f8dad9ae242c/proposals/NNNN-Normalize-Enum-Case-Representation.md&gt; you might also want to clean up the names of your cases. That means you have to perform a breaking API change.

Both proposals would be part of Swift 4. No module would be able to publish a production-ready version until Swift 4 is released. Also, `closed` wouldn’t have ABI implications until ABI is locked down, which won’t happen until Swift 4 is released (or later). So you would have the chance to clean up case names at the same time as you introduce `closed`.

It’s also worth pointing out that changing a case name is going to be a breaking change regardless of whether an enum is `closed` or not. It might be possible to minimize the impact of these changes if there was a way to specify the prior, deprecated name for the case, but this issue is orthogonal to `closed`.

···

On Feb 9, 2017, at 10:33 AM, Adrian Zubarev <adrian.zubarev@devandartist.com> wrote:
For example in one of my ‘just for fun’ projects I have two enum cases that look as follows:

case javaScript(String)
case scopedJavaScript(String, scope: Document)
If the mentioned proposal will be accepted I’d like to make these cases shiny.

// v1:
case javaScript(String)
case javaScript(String, scope: Document)
     
// or v2:
case javaScript(String, scope: Document? = nil)

--
Adrian Zubarev
Sent with Airmail

Am 9. Februar 2017 um 16:57:40, Matthew Johnson via swift-evolution (swift-evolution@swift.org <mailto:swift-evolution@swift.org>) schrieb:

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

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

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

The most important point IMO is that they *are* co-equal in the sense that they define a contract between library authors, library users and the compiler.

Certainly, `open` and your proposed `closed` both represent contracts among library authors, users, and the compiler. But so do other features that are not access modifiers--unless your intent is to make all the (not yet totally finalized) resilience attributes such as `@inlineable` become access modifiers. That both `open` and the proposed `closed` are both contracts doesn't make them both access modifiers.

This is a good point. But `open`, `closed` and `public` have something very important in common: they all have meaning with regarding the set of cases, subclasses or conforming types and are mutually exclusive in the contract they offer in this regard.

To me, the reason one _could_ justify `open` being spelled like an access modifier (which I was not supportive of, actually, though I was fine with `public` not allowing subclassing) is that it quacks like an access modifier in some ways. In particular, since it offers more "access" to a class than does `public` by allowing subclassing, one can argue that it fits at the top of a hierarchy of access levels.

As you define it, `closed` also makes additional guarantees to the end user than does `public` by (self-)imposing restrictions on the library author. Thus, it does not fit into a hierarchy of access modifiers where each level is more "accessible" than the next. Put another way, my point here is that `closed` is not the opposite of `open` in key ways, as the names might suggest. In fact, from the perspective of a library user, both `closed` and `open` would allow you to do more than `public`.

You make a great point here when you say that `closed` makes additional guarantees to the end user, beyond `public`. `closed` and `open` both give users more capabilities by placing additional burden on the library. In a very real sense `closed` *does* provide more visibility to information about the type - it guarantees knowledge of the *complete* set of cases, subclasses or protocols both now and in the future (modulo breaking changes). In this sense it can be said to be “more accessible than public”. This does form a strict hierarchy, just not a linear one:

            private
                  >
          fileprivate
                  >
            internal
                  >
             public
            / \
closed open

I admit that the name `closed` doesn’t *sounds* more accessible. Maybe there is a better name? But `closed` is the name we have all been using for this contract and it offers a nice symmetry with open. One opens the set of cases, subclasses, or conforming types to users. The other closes off the ability of the library to add to or hide any members of the set of cases, subclasses or conforming types. The symmetry is in the fact that they both say something about the totality of the set of cases, subclasses or conforming types.

It’s also worth noting that a `public` type can become `closed` or `open` in a future version of a library, but once `closed` or `open` that option is fixed forever (modulo breaking changes). This also suggests the hierarchy I visualized above.

As you note, there are some differences in how the `closed` contract is supported. But that is far less important than the meaning of the contract itself.

Since the additional guarantees of both `open` and your proposed `closed` impose burdens on the library _author_ and offer more flexibility to the library _user_, I feel it is highly misleading to make them co-equal but antonyms. They are not the "opposite" of each other and the spelling would be misleading.

They are not exactly antonyms, but if you think about this in terms of the set of cases, subclasses or conforming types their obvious meaning does make sense. `closed` says to users: “you know the complete set”. `open` says to users: “you are allowed to add to the set”.

I suppose this suggests a possible alternative name: `complete`. That would avoid the antonym relationship and maybe be more accurate.

Dave's comment about tools to assist with contract-compatible API evolution is the right way to think about this. Of course you *can* make breaking changes, but we want to make it clear when you *are* making a breaking change, both for source and for ABI compatibility. This will help library authors, but it also helps users as well as the compiler reason about code when we are able to offer stronger guarantees.

Yes, this is totally fair.

Most notably, the behavior of public enums *already* has the API contract of `closed` and we do not want to remove that capability. This proposal only formalizes how that contract is specified and makes it consistent across all kinds of types. It *does not* introduce the idea of a closed semantic contract for a type.

As this change didn't seem in scope for Swift 4 phase 1, I've held off on discussing my own thoughts on access levels. The idea I was going to propose in phase 2 was to have simply open and public enums (and protocols). I really think that completes access levels in a rational way without introducing another keyword.

The reason I posted now is because formalizing this API contract for enums must happen before ABI is locked down, and also because there is at least one protocol in the standard library (`MirrorPath`) which is documented with the intent that it be `closed`.

I understand the reluctance to introduce another keyword. It isn’t clear to me what semantics you assign to `open` and `public` enums.

Are you suggesting that they match the semantics defined in my proposal and suggesting closed enums (i.e. matching the current behavior of `public` enums) would require an `@closed` annotation as suggested in the Library Evolution document?

Yes, I am.

This has the effect of defaulting to `public` behavior. It will lead to unnecessary boilerplate-y annotations in our code. One of the big drivers behind the decision to make `open` an access modifier is to avoid this kind of boilerplate-y annotation. Why should we have to write `@closed public` when simply saying `closed` (or `complete`) unambiguously communicates the same thing?

I am opposed to this approach because it penalizes the API contract that I think is often the most appropriate for enums. I strongly prefer that we adopt the same neutral stance that we when we introduced `open`.

I would not characterize `open` as a neutral stance. But that's neither here nor there.

What I meant by “neutral” is that both `open` and `public` carry the same syntactic weight. The language does not make one or the other more convenient for the library author and requires them to make an explicit choice.

The problem (as I see it) with your argument is that, in general, the following two thoughts are incompatible: (a) the additional burden of a public API contract should be opt-in; vs. (b) there should be neutrality as to whether or not one assumes the burden of a particular public API contract.

Opting in means that one has made the deliberate effort of rejecting some sort of more natural or default choice. Writing `@closed public enum` can be easily thought of as opting in, because doing so very clearly requires actively choosing to add something more than the alternative `public enum`.

Writing `open class` is harder to justify as opting in, because it is not as obvious that `public class` is some sort of default. (Hence, why I did not think that `open` should have been an access level, though I was fine with the proposal otherwise.) The saving grace there is that, in the linear hierarchy of access levels, `open` is two steps away from the default access level of `internal`, whereas `public` is only one step removed. So, one can make the passable rationalization that in choosing `open` one is escalating from `internal` to `public` to `open`--i.e., that going the extra step can be regarded as the act of opting in by choosing not to stop at `public`.

As I see it, one can only be said to opt in to B (vs. an alternative A) only to the extent that A and B are not neutral choices but rather in some sort of hierarchy. Again, I'm unconvinced `closed` fits into a linear hierarchy of access modifiers, and therefore I see spelling `closed` like `open` as problematic.

I think part of our disagreement is in the definition of opting in. What *I* mean by that is that the compiler does not make a choice for the user because they omitted an annotation. We require a class visible outside the module to chooses `open` or chooses `public`. By making that choice it opts-in to one semantic or the other. There is no "deliberate effort to reject a more natural choice” required at all. All that is required is that intent is unambiguous.

However, the same logic you used for `open` also works for `closed` using the hierarchy I visualized above. I find the logic of that hierarchy very convincing (regardless of the final name we choose for `closed`).

···

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

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

On the other hand, you might be suggesting that `public` enums maintain their current behavior and we simply introduce `open` as a modifier that reserves the right for the *library* to introduce new cases while continuing to prohibit *users* from introducing new cases. This approach has inconsistent semantics for both `public` and `open`. These keywords would indicate a different API contract for enums than they do for classes and protocols. In fact, `open` for enums would have a contract analagous with `public` for classes and protocols. This feels like a recipe for confusion. IMO, having consistent semantics for each keyword is pretty important. We already have, and desire to continue to have, three distinct semantic contracts. If we want keywords with consistent semantics we are going to have to introduce a new keyword for the third meaning.

On Wed, Feb 8, 2017 at 17:05 Matthew Johnson via swift-evolution <swift-evolution@swift.org <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

I admit that I have not followed all of this discussion, but as far as I see, we could equally well do this by calling “not-open” enum’s “final”.
It seems like a better fit to me.

That is actually not a very good fit at all. `final` means a class cannot have any subclasses. If we applied it to enums I think the closest parallel would be an enum with no cases. But this makes the enum uninhabited so it is not just like `final class`, but actually more like `abstract final class`.

···

Sent from my iPad

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

Regards,
Rien

Site: http://balancingrock.nl
Blog: http://swiftrien.blogspot.com
Github: Balancingrock (Rien) · GitHub
Project: http://swiftfire.nl

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

Sent from my iPad

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

I’m probably better describing things with some bikeshedding code, but feel free to criticize it as much as you’d like.

//===========--------- Module A ---------===========//
@closed public enum A {
   case a
}

extension A {
   case aa // error, because enum is closed
}

This is an error because you can't add cases in an extension. I imagine this is how cases would be added outside the module if we allow `open enum` in the future. But whether or not this is allowed *within* the module is a separate question that is orthogonal to `closed` and `open`.

public func foo(a: A) {
   switch a {
   case .a:
       print("done")
   }
}

public enum B {
   case b
}

extension B {
   case bb // fine, because not-closed enums are extensible
}

As noted above, whether this is allowed or not *within* the module is orthogonal to `closed`. *Outside* the module it would only be possible for enum declared `open` (if we add this feature in the future).

public func bar(b: B) {
   switch b {
   case .b:
       print("b")

   default: // always needed
       print("some other case")
   }
}

// Sub-enum relationships

// Possible even the enum A is closed, because `@closed` only
// closes the extensibility of an enum
enum SubA : A {
   case aa
}

Now you're talking about value subtypes. That is orthogonal. Also, this syntax already has a meaning (the raw value of the enum is A) so we wouldn't be able to use it the way you are intending here. Finally, it is misleading syntax because what you mean here is "A is a subtype of SubA" which is exactly the opposite of what the syntax implies.

All values of A are valid values of SubA, but SubA has values that are not valid values of A.

// The following enum can have a sub-enum in the clients module
open enum C {
   case c
}

public func cool(c: C) {
   switch c {
   case .c:
       print("c")

   default: // always needed
       print("some other case")
   }
}

@closed open enum D {
   case d
}

public func doo(d: D) {
   switch b {
   case .b:
       print("b")
   }
}

// The enum case is always known at any point, no matter
// where the instance comes from, right?

let subA = SubA.aa
let otherSubA = SubA.a // Inherited case

let a: A = subA // error, downgrade the sub-enum to A first
let a: A = otherSubA // okay

foo(a: subA) // error, downgrade the sub-enum to A first
foo(a: otherSubA) // okay

//===========--------- Module B ---------===========//

// Totally fine
switch A.a {
case .a:
   print("done")
}

extension A {
   case aa // not allowed because the enum is closed
}

extension B {
   case bbb
}

switch B.b {
case .b:
   print("b")
default:
   print("somethine else")
}

bar(b: B.bbb) // fine, because the switch statement on enums without
// `@closed` has always`default`

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

let subC =
SubC.cc

cool(c: subC) // okay

enum SubD : D {
   case dd
}

doo(d: D.dd)// error, downgrade sub-enum to D first

My point here is, that we should not think of (possible) open enums as enums that the client is allowed to extend. That way we’re only creating another inconsistent case for the open access modifier. As far as I can tell, open as for today means “the client is allowed to subclass/override things from a different module”.

Yes, but subclasses are analogous to enum cases. A subtype of an enum would remove cases. I think you are misunderstanding the relationship of enums to classes and protocols.

And I already said it hundred of times that we should extend this to make open a true access modifier in Swift. That said the meaning of open should become:

   • The client is allowed to sub-type (currently only classes are supported).
   • The client is allowed to conform to open protocols
   • The client is allowed to override open type members
This also means that extensibility is still allowed to public types. Public-but-not-open classes are still extensible today, which is the correct behavior. Extending an enum which is not closed could or probably should be made possible through extensions, because I cannot think of anther elegant way for the client to do so.

This is what `open enum` would allow. It is the proper enum analogue of open classes.

That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above).

Value subtyping is very interesting. I have been working on some ideas around this but I want to keep this thread focused.

If I’m not mistaken, every enum case is known at compile time,

This is true today but will not always be true in the future. That is in large part what this thread is about.

which means to me that we can safely check the case before allowing to assign or pass an instance of a sub-enum to some of its super-enum. (Downgrading an enum case means that you will have to write some code that either mutates your current instance or creates a new one which matches one of the super-enum cases.) Furthermore that allows a clear distinction of what open access modifier does and how @closed behaves.

I'm not going to comment on the rest because it is premised on a misunderstanding of what value subtyping is. I'm going to share some ideas around value subtyping in a new thread as soon as I have a chance to finish putting them together.

To summarize:

   • @closed enum - you’re not allowed to add new cases to the enum in your lib + (you’re allowed to create sub-enums)
   • @closed public enum - you and the client are not allowed to add new cases (+ the client is not allowed to create sub-enums)
   • @closed open enum - you and the client are not allowed to add new cases (+ the client might create new sub-enums)
   • enum - you’re allowed to add new cases (default is needed in switch statements) (+ you can create new sub-enums)
   • public enum - you and the client are allowed to add new cases (+ only you are allowed to create new sub-enums)
   • open enum - you and the client are allowed to add new cases (everyone can create new sub-enums)
This is a lot of bike shedding of mine, and the idea might not even see any light in Swift at all, but I’d like to share my ideas with the community. Feel free to criticize them or flesh something out into something real. :)

P.S.: If we had something like this:

@closed enum X {
   case x, y
   func foo() {
    switch self {
       case .x, .y:
           print("swift")
   }
}

enum Z : X {
   case z, zz
   override func foo() {
       // Iff `self` is `z` or `zz` then calling super will result in an error.
       // Possible solution: always tell the client to downgrade explicitly the
       // case first if there is an attempt to call super (if mutating),
       // or handle all cases

       switch self {
       case .z, .zz:
           print("custom work")
       default: // or all super-enum cases
           super.foo()
       }
   }
}

--
Adrian Zubarev
Sent with Airmail

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

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:
I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums. I believe we should move further in the direction we took when introducing the `open` keyword. I have identified what I think is a promising direction and am interested in feedback from the community. If community feedback is positive I will flesh this out into a more complete proposal draft.

Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module. The decision we reached was to avoid having a default at all, and instead make `open` an access modifier. The result is library authors are required to consider the behavior they wish for each class. Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html\) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not. The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior. The Library Evolution document (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums\) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation. This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols. In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types. There are at least two reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice. Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol. Enums are not a substitute for this use case. The only option is to resort to documentation and runtime checks.

Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`. This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier. The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances. (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances. The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier. The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances. Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances. A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that. A class that conforms to a `closed` protocol also need not be `closed`. It may also be `open`. Finally, a protocol that refines a `closed` protocol need not be `closed`. It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be. It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols. The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.

Source compatibility:

This proposal affects both public enums and public protocols. The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal. Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4. We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior. Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else. I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error. `public protocol` is not allowed. Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics. `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

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

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the access modifier hierarchy you're proposing and adopt or reject with that clarity, we'll be fine whether we go with `closed` or with `@closed`. But I don't think the source compatibility strategy you list is the most simple or the most easy to understand for end users.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.
- With respect to enums, I don't think we need such a drastic whiplash in terms of what will compile in future versions. Instead, we could take a more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases in switch statements over public enums. Simultaneously, add `closed` or `@closed` (whatever is the approved spelling) and start annotating standard library APIs. The annotation will be purely future-proofing and have no functional effect (i.e. the compiler will do nothing differently for a `closed enum` or `@closed public enum` (as the case may be) versus a plain `public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have a `default` statement: offer a fix-it to insert `default: fatalError()` and, if the enum is in the same project, offer a fix-it to insert `closed` or `@closed`.
3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a switch statement over a public enum doesn't have a `default` statement. Now, new syntax to extend an `open enum` can be introduced and the compiler can treat closed and public enums differently.

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

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

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

Sent from my iPad

>
> I admit that I have not followed all of this discussion, but as far as I
see, we could equally well do this by calling “not-open” enum’s “final”.
> It seems like a better fit to me.

That is actually not a very good fit at all. `final` means a class cannot
have any subclasses. If we applied it to enums I think the closest
parallel would be an enum with no cases. But this makes the enum
uninhabited so it is not just like `final class`, but actually more like
`abstract final class`.

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

···

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

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

>
> Regards,
> Rien
>
> Site: http://balancingrock.nl
> Blog: http://swiftrien.blogspot.com
> Github: Balancingrock (Rien) · GitHub
> Project: http://swiftfire.nl
>
>
>
>
>
>> On 11 Feb 2017, at 14:07, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:
>>
>>
>>
>> Sent from my iPad
>>
>>> On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution < > swift-evolution@swift.org> wrote:
>>>
>>> I’m probably better describing things with some bikeshedding code, but
feel free to criticize it as much as you’d like.
>>>
>>> //===========--------- Module A ---------===========//
>>> @closed public enum A {
>>> case a
>>> }
>>>
>>> extension A {
>>> case aa // error, because enum is closed
>>> }
>>>
>> This is an error because you can't add cases in an extension. I
imagine this is how cases would be added outside the module if we allow
`open enum` in the future. But whether or not this is allowed *within* the
module is a separate question that is orthogonal to `closed` and `open`.
>>
>>
>>>
>>> public func foo(a: A) {
>>> switch a {
>>> case .a:
>>> print("done")
>>> }
>>> }
>>>
>>> public enum B {
>>> case b
>>> }
>>>
>>> extension B {
>>> case bb // fine, because not-closed enums are extensible
>>> }
>>>
>> As noted above, whether this is allowed or not *within* the module is
orthogonal to `closed`. *Outside* the module it would only be possible for
enum declared `open` (if we add this feature in the future).
>>
>>>
>>> public func bar(b: B) {
>>> switch b {
>>> case .b:
>>> print("b")
>>>
>>> default: // always needed
>>> print("some other case")
>>> }
>>> }
>>>
>>> // Sub-enum relationships
>>>
>>> // Possible even the enum A is closed, because `@closed` only
>>> // closes the extensibility of an enum
>>> enum SubA : A {
>>> case aa
>>> }
>>>
>>>
>> Now you're talking about value subtypes. That is orthogonal. Also,
this syntax already has a meaning (the raw value of the enum is A) so we
wouldn't be able to use it the way you are intending here. Finally, it is
misleading syntax because what you mean here is "A is a subtype of SubA"
which is exactly the opposite of what the syntax implies.
>>
>> All values of A are valid values of SubA, but SubA has values that are
not valid values of A.
>>
>>> // The following enum can have a sub-enum in the clients module
>>> open enum C {
>>> case c
>>> }
>>>
>>> public func cool(c: C) {
>>> switch c {
>>> case .c:
>>> print("c")
>>>
>>> default: // always needed
>>> print("some other case")
>>> }
>>> }
>>>
>>> @closed open enum D {
>>> case d
>>> }
>>>
>>> public func doo(d: D) {
>>> switch b {
>>> case .b:
>>> print("b")
>>> }
>>> }
>>>
>>> // The enum case is always known at any point, no matter
>>> // where the instance comes from, right?
>>>
>>> let subA = SubA.aa
>>> let otherSubA = SubA.a // Inherited case
>>>
>>> let a: A = subA // error, downgrade the sub-enum to A first
>>> let a: A = otherSubA // okay
>>>
>>> foo(a: subA) // error, downgrade the sub-enum to A first
>>> foo(a: otherSubA) // okay
>>>
>>> //===========--------- Module B ---------===========//
>>>
>>> // Totally fine
>>> switch A.a {
>>> case .a:
>>> print("done")
>>> }
>>>
>>> extension A {
>>> case aa // not allowed because the enum is closed
>>> }
>>>
>>> extension B {
>>> case bbb
>>> }
>>>
>>> switch B.b {
>>> case .b:
>>> print("b")
>>> default:
>>> print("somethine else")
>>> }
>>>
>>> bar(b: B.bbb) // fine, because the switch statement on enums without
>>> // `@closed` has always`default`
>>>
>>> // Allowed because `C` is open, and open allows sub-typing, conforming
>>> // and overriding to the client
>>> enum SubC : C {
>>> case cc
>>> }
>>>
>>> let subC =
>>> SubC.cc
>>>
>>>
>>> cool(c: subC) // okay
>>>
>>> enum SubD : D {
>>> case dd
>>> }
>>>
>>> doo(d: D.dd)// error, downgrade sub-enum to D first
>>>
>>> My point here is, that we should not think of (possible) open enums as
enums that the client is allowed to extend. That way we’re only creating
another inconsistent case for the open access modifier. As far as I can
tell, open as for today means “the client is allowed to subclass/override
things from a different module”.
>>>
>> Yes, but subclasses are analogous to enum cases. A subtype of an enum
would remove cases. I think you are misunderstanding the relationship of
enums to classes and protocols.
>>
>>> And I already said it hundred of times that we should extend this to
make open a true access modifier in Swift. That said the meaning of open
should become:
>>>
>>> • The client is allowed to sub-type (currently only classes are
supported).
>>> • The client is allowed to conform to open protocols
>>> • The client is allowed to override open type members
>>> This also means that extensibility is still allowed to public types.
Public-but-not-open classes are still extensible today, which is the
correct behavior. Extending an enum which is not closed could or probably
should be made possible through extensions, because I cannot think of
anther elegant way for the client to do so.
>>>
>> This is what `open enum` would allow. It is the proper enum analogue
of open classes.
>>
>>> That will leave us the possibility to think of sub-typing enums in the
future (I sketched it out a little above).
>>>
>> Value subtyping is very interesting. I have been working on some ideas
around this but I want to keep this thread focused.
>>
>>> If I’m not mistaken, every enum case is known at compile time,
>>>
>> This is true today but will not always be true in the future. That is
in large part what this thread is about.
>>
>>> which means to me that we can safely check the case before allowing to
assign or pass an instance of a sub-enum to some of its super-enum.
(Downgrading an enum case means that you will have to write some code that
either mutates your current instance or creates a new one which matches one
of the super-enum cases.) Furthermore that allows a clear distinction of
what open access modifier does and how @closed behaves.
>>>
>> I'm not going to comment on the rest because it is premised on a
misunderstanding of what value subtyping is. I'm going to share some ideas
around value subtyping in a new thread as soon as I have a chance to finish
putting them together.
>>
>>> To summarize:
>>>
>>> • @closed enum - you’re not allowed to add new cases to the enum in
your lib + (you’re allowed to create sub-enums)
>>> • @closed public enum - you and the client are not allowed to add
new cases (+ the client is not allowed to create sub-enums)
>>> • @closed open enum - you and the client are not allowed to add new
cases (+ the client might create new sub-enums)
>>> • enum - you’re allowed to add new cases (default is needed in
switch statements) (+ you can create new sub-enums)
>>> • public enum - you and the client are allowed to add new cases (+
only you are allowed to create new sub-enums)
>>> • open enum - you and the client are allowed to add new cases
(everyone can create new sub-enums)
>>> This is a lot of bike shedding of mine, and the idea might not even
see any light in Swift at all, but I’d like to share my ideas with the
community. Feel free to criticize them or flesh something out into
something real. :)
>>>
>>> P.S.: If we had something like this:
>>>
>>> @closed enum X {
>>> case x, y
>>> func foo() {
>>> switch self {
>>> case .x, .y:
>>> print("swift")
>>> }
>>> }
>>>
>>> enum Z : X {
>>> case z, zz
>>> override func foo() {
>>> // Iff `self` is `z` or `zz` then calling super will result in
an error.
>>> // Possible solution: always tell the client to downgrade
explicitly the
>>> // case first if there is an attempt to call super (if
mutating),
>>> // or handle all cases
>>>
>>> switch self {
>>> case .z, .zz:
>>> print("custom work")
>>> default: // or all super-enum cases
>>> super.foo()
>>> }
>>> }
>>> }
>>>
>>>
>>>
>>>
>>> --
>>> Adrian Zubarev
>>> Sent with Airmail
>>>
>>> Am 11. Februar 2017 um 04:49:11, Xiaodi Wu via swift-evolution (
swift-evolution@swift.org) schrieb:
>>>
>>>> On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:
>>>> I’ve been thinking a lot about our public access modifier story
lately in the context of both protocols and enums. I believe we should
move further in the direction we took when introducing the `open` keyword.
I have identified what I think is a promising direction and am interested
in feedback from the community. If community feedback is positive I will
flesh this out into a more complete proposal draft.
>>>>
>>>>
>>>> Background and Motivation:
>>>>
>>>> In Swift 3 we had an extended debate regarding whether or not to
allow inheritance of public classes by default or to require an annotation
for classes that could be subclassed outside the module. The decision we
reached was to avoid having a default at all, and instead make `open` an
access modifier. The result is library authors are required to consider
the behavior they wish for each class. Both behaviors are equally
convenient (neither is penalized by requiring an additional boilerplate-y
annotation).
>>>>
>>>> A recent thread (The swift-evolution Archives
Week-of-Mon-20170206/031566.html) discussed a similar tradeoff regarding
whether public enums should commit to a fixed set of cases by default or
not. The current behavior is that they *do* commit to a fixed set of cases
and there is no option (afaik) to modify that behavior. The Library
Evolution document (https://github.com/apple/swift/blob/master/docs/
LibraryEvolution.rst#enums) suggests a desire to change this before
locking down ABI such that public enums *do not* make this commitment by
default, and are required to opt-in to this behavior using an `@closed`
annotation.
>>>>
>>>> In the previous discussion I stated a strong preference that closed
enums *not* be penalized with an additional annotation. This is because I
feel pretty strongly that it is a design smell to: 1) expose cases publicly
if consumers of the API are not expected to switch on them and 2) require
users to handle unknown future cases if they are likely to switch over the
cases in correct use of the API.
>>>>
>>>> The conclusion I came to in that thread is that we should adopt the
same strategy as we did with classes: there should not be a default.
>>>>
>>>> There have also been several discussions both on the list and via
Twitter regarding whether or not we should allow closed protocols. In a
recent Twitter discussion Joe Groff suggested that we don’t need them
because we should use an enum when there is a fixed set of conforming
types. There are at least two reasons why I still think we *should* add
support for closed protocols.
>>>>
>>>> As noted above (and in the previous thread in more detail), if the
set of types (cases) isn’t intended to be fixed (i.e. the library may add
new types in the future) an enum is likely not a good choice. Using a
closed protocol discourages the user from switching and prevents the user
from adding conformances that are not desired.
>>>>
>>>> Another use case supported by closed protocols is a design where
users are not allowed to conform directly to a protocol, but instead are
required to conform to one of several protocols which refine the closed
protocol. Enums are not a substitute for this use case. The only option
is to resort to documentation and runtime checks.
>>>>
>>>>
>>>> Proposal:
>>>>
>>>> This proposal introduces the new access modifier `closed` as well as
clarifying the meaning of `public` and expanding the use of `open`. This
provides consistent capabilities and semantics across enums, classes and
protocols.
>>>>
>>>> `open` is the most permissive modifier. The symbol is visible
outside the module and both users and future versions of the library are
allowed to add new cases, subclasses or conformances. (Note: this proposal
does not introduce user-extensible `open` enums, but provides the syntax
that would be used if they are added to the language)
>>>>
>>>> `public` makes the symbol visible without allowing the user to add
new cases, subclasses or conformances. The library reserves the right to
add new cases, subclasses or conformances in a future version.
>>>>
>>>> `closed` is the most restrictive modifier. The symbol is visible
publicly with the commitment that future versions of the library are *also*
prohibited from adding new cases, subclasses or conformances.
Additionally, all cases, subclasses or conformances must be visible outside
the module.
>>>>
>>>> Note: the `closed` modifier only applies to *direct* subclasses or
conformances. A subclass of a `closed` class need not be `closed`, in fact
it may be `open` if the design of the library requires that. A class that
conforms to a `closed` protocol also need not be `closed`. It may also be
`open`. Finally, a protocol that refines a `closed` protocol need not be
`closed`. It may also be `open`.
>>>>
>>>> This proposal is consistent with the principle that libraries should
opt-in to all public API contracts without taking a position on what that
contract should be. It does this in a way that offers semantically
consistent choices for API contract across classes, enums and protocols.
The result is that the language allows us to choose the best tool for the
job without restricting the designs we might consider because some kinds of
types are limited with respect to the `open`, `public` and `closed`
semantics a design might require.
>>>>
>>>>
>>>> Source compatibility:
>>>>
>>>> This proposal affects both public enums and public protocols. The
current behavior of enums is equivalent to a `closed` enum under this
proposal and the current behavior of protocols is equivalent to an `open`
protocol under this proposal. Both changes allow for a simple mechanical
migration, but that may not be sufficient given the source compatibility
promise made for Swift 4. We may need to identify a multi-release strategy
for adopting this proposal.
>>>>
>>>> Brent Royal-Gordon suggested such a strategy in a discussion
regarding closed protocols on Twitter:
>>>>
>>>> * In Swift 4: all unannotated public protocols receive a warning,
possibly with a fix-it to change the annotation to `open`.
>>>> * Also in Swift 4: an annotation is introduced to opt-in to the new
`public` behavior. Brent suggested `@closed`, but as this proposal
distinguishes `public` and `closed` we would need to identify something
else. I will use `@annotation` as a placeholder.
>>>> * Also In Swift 4: the `closed` modifier is introduced.
>>>>
>>>> * In Swift 5 the warning becomes a compiler error. `public protocol`
is not allowed. Users must use `@annotation public protocol`.
>>>> * In Swift 6 `public protocol` is allowed again, now with the new
semantics. `@annotation public protocol` is also allowed, now with a
warning and a fix-it to remove the warning.
>>>> * In Swift 7 `@annotation public protocol` is no longer allowed.
>>>>
>>>> A similar mult-release strategy would work for migrating public enums.
>>>>
>>>> A different line of feedback here:
>>>>
>>>> As per previous reply, I now think if we clarify the mental model of
the access modifier hierarchy you're proposing and adopt or reject with
that clarity, we'll be fine whether we go with `closed` or with `@closed`.
But I don't think the source compatibility strategy you list is the most
simple or the most easy to understand for end users.
>>>>
>>>> - I'll leave aside closed protocols, which as per Jordan Rose's
feedback may or may not have sufficient interestingness.
>>>> - With respect to enums, I don't think we need such a drastic
whiplash in terms of what will compile in future versions. Instead, we
could take a more pragmatic approach:
>>>>
>>>> 1. In Swift 4, remove the warning (or is it error?) about `default`
cases in switch statements over public enums. Simultaneously, add `closed`
or `@closed` (whatever is the approved spelling) and start annotating
standard library APIs. The annotation will be purely future-proofing and
have no functional effect (i.e. the compiler will do nothing differently
for a `closed enum` or `@closed public enum` (as the case may be) versus a
plain `public enum`).
>>>> 2. In Swift 4.1, _warn_ if switch statements over public enums don't
have a `default` statement: offer a fix-it to insert `default:
fatalError()` and, if the enum is in the same project, offer a fix-it to
insert `closed` or `@closed`.
>>>> 3. In Swift 5, upgrade the warning to an error for non-exhaustiveness
if a switch statement over a public enum doesn't have a `default`
statement. Now, new syntax to extend an `open enum` can be introduced and
the compiler can treat closed and public enums differently.
>>>>
>>>> _______________________________________________
>>>> swift-evolution mailing list
>>>> swift-evolution@swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution@swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>

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