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

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


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

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

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

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

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

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

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

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

And in Swift:

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

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

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

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

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

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

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

Jordan


> On Feb 8, 2017, at 15:05, Matthew Johnson via swift-evolution 
> <swift-evolution@swift.org> wrote:
> 
> I’ve been thinking a lot about our public access modifier story lately in the 
> context of both protocols and enums.  I believe we should move further in the 
> direction we took when introducing the `open` keyword.  I have identified 
> what I think is a promising direction and am interested in feedback from the 
> community.  If community feedback is positive I will flesh this out into a 
> more complete proposal draft.
> 
> 
> Background and Motivation:
> 
> In Swift 3 we had an extended debate regarding whether or not to allow 
> inheritance of public classes by default or to require an annotation for 
> classes that could be subclassed outside the module.  The decision we reached 
> was to avoid having a default at all, and instead make `open` an access 
> modifier.  The result is library authors are required to consider the 
> behavior they wish for each class.  Both behaviors are equally convenient 
> (neither is penalized by requiring an additional boilerplate-y annotation).
> 
> A recent thread 
> (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html
>  
> <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 
> <https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums>) 
> suggests a desire to change this before locking down ABI such that public 
> enums *do not* make this commitment by default, and are required to opt-in to 
> this behavior using an `@closed` annotation.
> 
> In the previous discussion I stated a strong preference that closed enums 
> *not* be penalized with an additional annotation.  This is because I feel 
> pretty strongly that it is a design smell to: 1) expose cases publicly if 
> consumers of the API are not expected to switch on them and 2) require users 
> to handle unknown future cases if they are likely to switch over the cases in 
> correct use of the API.
> 
> The conclusion I came to in that thread is that we should adopt the same 
> strategy as we did with classes: there should not be a default.
> 
> There have also been several discussions both on the list and via Twitter 
> regarding whether or not we should allow closed protocols.  In a recent 
> Twitter discussion Joe Groff suggested that we don’t need them because we 
> should use an enum when there is a fixed set of conforming types.  There are 
> at least two  reasons why I still think we *should* add support for closed 
> protocols.
> 
> As noted above (and in the previous thread in more detail), if the set of 
> types (cases) isn’t intended to be fixed (i.e. the library may add new types 
> in the future) an enum is likely not a good choice.  Using a closed protocol 
> discourages the user from switching and prevents the user from adding 
> conformances that are not desired.
> 
> Another use case supported by closed protocols is a design where users are 
> not allowed to conform directly to a protocol, but instead are required to 
> conform to one of several protocols which refine the closed protocol.  Enums 
> are not a substitute for this use case.  The only option is to resort to 
> documentation and runtime checks.
> 
> 
> Proposal:
> 
> This proposal introduces the new access modifier `closed` as well as 
> clarifying the meaning of `public` and expanding the use of `open`.  This 
> provides consistent capabilities and semantics across enums, classes and 
> protocols.
> 
> `open` is the most permissive modifier.  The symbol is visible outside the 
> module and both users and future versions of the library are allowed to add 
> new cases, subclasses or conformances.  (Note: this proposal does not 
> introduce user-extensible `open` enums, but provides the syntax that would be 
> used if they are added to the language)
> 
> `public` makes the symbol visible without allowing the user to add new cases, 
> subclasses or conformances.  The library reserves the right to add new cases, 
> subclasses or conformances in a future version.
> 
> `closed` is the most restrictive modifier.  The symbol is visible publicly 
> with the commitment that future versions of the library are *also* prohibited 
> from adding new cases, subclasses or conformances.  Additionally, all cases, 
> subclasses or conformances must be visible outside the module.
> 
> Note: the `closed` modifier only applies to *direct* subclasses or 
> conformances.  A subclass of a `closed` class need not be `closed`, in fact 
> it may be `open` if the design of the library requires that.  A class that 
> conforms to a `closed` protocol also need not be `closed`.  It may also be 
> `open`.  Finally, a protocol that refines a `closed` protocol need not be 
> `closed`.  It may also be `open`.
> 
> This proposal is consistent with the principle that libraries should opt-in 
> to all public API contracts without taking a position on what that contract 
> should be.  It does this in a way that offers semantically consistent choices 
> for API contract across classes, enums and protocols.  The result is that the 
> language allows us to choose the best tool for the job without restricting 
> the designs we might consider because some kinds of types are limited with 
> respect to the `open`, `public` and `closed` semantics a design might require.
> 
> 
> Source compatibility:
> 
> This proposal affects both public enums and public protocols.  The current 
> behavior of enums is equivalent to a `closed` enum under this proposal and 
> the current behavior of protocols is equivalent to an `open` protocol under 
> this proposal.  Both changes allow for a simple mechanical migration, but 
> that may not be sufficient given the source compatibility promise made for 
> Swift 4.  We may need to identify a multi-release strategy for adopting this 
> proposal.
> 
> Brent Royal-Gordon suggested such a strategy in a discussion regarding closed 
> protocols on Twitter:
> 
> * In Swift 4: all unannotated public protocols receive a warning, possibly 
> with a fix-it to change the annotation to `open`.
> * Also in Swift 4: an annotation is introduced to opt-in to the new `public` 
> behavior.  Brent suggested `@closed`, but as this proposal distinguishes 
> `public` and `closed` we would need to identify something else.  I will use 
> `@annotation` as a placeholder.
> * Also In Swift 4: the `closed` modifier is introduced.
> 
> * In Swift 5 the warning becomes a compiler error.  `public protocol` is not 
> allowed.  Users must use `@annotation public protocol`.
> * In Swift 6 `public protocol` is allowed again, now with the new semantics.  
> `@annotation public protocol` is also allowed, now with a warning and a 
> fix-it to remove the warning.
> * In Swift 7 `@annotation public protocol` is no longer allowed.
> 
> A similar mult-release strategy would work for migrating public enums.
> 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

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

Reply via email to