I’d very much in favour of a consistent access modifiers across the whole 
language and eliminate exclusive `open`. `open/public` protocols are more than 
welcome. Plus it’s already has been said that Swift will potentially support 
subtyping for value type in some future, where we’ll yet again would need to 
align what `public` and `open` will mean. So I’d appreciate all the steps that 
could already be made now to align their meaning as much as it’s possible to 
this moment.


Am 9. August 2017 um 18:08:09, Matthew Johnson via swift-evolution 
(swift-evolution@swift.org) schrieb:

Hi Jordan,

Thanks for bringing this topic up again!  I’m glad to see it will receive 
attention in Swift 5.  I agree with the semantics of your proposed direction.  

In terms of syntax, I continue to believe that requiring users to specify a 
keyword indicating open or closed *in addition* to public would be unfortunate. 
 Open / closed is only relevant for public enums and therefore implies public.  
We’ve done a really good job of avoiding keyword soup in Swift and the way that 
open classes are implicitly public is a good precedent that we should follow.

I also continue to believe that aligning protocols, enums and classes to use 
consistent terminology for similar concepts has many advantages.  The semantics 
would be:

* open: Extensible outside the library
* public: Extensible in future versions of the library (or privately by the 
library)
* closed: Fixed set of publicly visible cases / subclasses / conformances 
defined by the library and guaranteed not to change without breaking ABI and 
source compatibility.

This approach makes public a “soft default” that preserves maximum flexibility 
for the library author while allowing them to make a stronger guarantee of 
user-extensibility or completeness by changing (rather than adding) a keyword.  
It also highlights the symmetry of the two very different user-guarantees a 
library may choose to support.

As noted in my previous thread, this approach would require a migration for 
protocols as well as enums as the current behavior of public protocols is to 
allow conformances outside the library.

There are certainly reasonable arguments to be made for other approaches, 
particularly if there is no appetite for changing the semantics of public 
protocols (which seems likely).  Nevertheless, I think we should keep the 
merits of consistency in mind and understand the benefits of alternatives 
relative to the more consistent approach as we evaluate them.

In terms of alternatives, what is your opinion on using public as a “soft 
default” and assigning it one of the two enum semantics you discuss?  Do you 
think this makes sense or would you prefer distinct keywords for these two 
semantics?  I don’t have any really great new ideas, but I’ll throw out 
“complete” and “incomplete” as a possibility.

My two cents for now…

Matthew


On Aug 8, 2017, at 5:27 PM, Jordan Rose via swift-evolution 
<swift-evolution@swift.org> wrote:

Hi, everyone. Now that Swift 5 is starting up, I'd like to circle back to an 
issue that's been around for a while: the source compatibility of enums. Today, 
it's an error to switch over an enum without handling all the cases, but this 
breaks down in a number of ways:

- A C enum may have "private cases" that aren't defined inside the original 
enum declaration, and there's no way to detect these in a switch without 
dropping down to the rawValue.
- For the same reason, the compiler-synthesized 'init(rawValue:)' on an 
imported enum never produces 'nil', because who knows how anyone's using C 
enums anyway?
- Adding a new case to a Swift enum in a library breaks any client code that 
was trying to switch over it.

(This list might sound familiar, and that's because it's from a message of mine 
on a thread started by Matthew Johnson back in February called "[Pitch] 
consistent public access modifiers". Most of the rest of this email is going to 
go the same way, because we still need to make progress here.)

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.


Behavior

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

- When enums are imported from Apple frameworks, they should always require a 
default case, 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 allow exhaustive switches.

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 allow 
exhaustive switching, because there are no compatibility issues, or not, 
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 allow exhaustive 
switching, because there are no binary compatibility issues, or not, 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 
allow exhaustive switching, because I might have defined it myself, or not, 
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 allow 
exhaustive switching, because there are no binary compatibility issues, or not, 
because there may be source compatibility issues? Again, 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. I've been talking about 
us disallowing exhaustive switching, i.e. "if the enum might grow new cases 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 framework enums you should exhaustively switch 
over anyway. (Think about Apple's frameworks again.) I don't have a great 
answer, though.

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. We didn't think this was worth the complexity.


Terminology

The "Library Evolution" doc (mostly written by me) originally called these 
"open" and "closed" enums ("requires a default" and "allows exhaustive 
switching", respectively), but this predated the use of 'open' to describe 
classes and class members. Matthew's original thread did suggest using 'open' 
for enums as well, but I argued against that, for a few reasons:

- For classes, "open" and "non-open" restrict what the client can do. For 
enums, it's more about providing the client with additional guarantees—and 
"non-open" is the one with more guarantees.
- The "safe" default is backwards: a merely-public class can be made 'open', 
while an 'open' class cannot be made non-open. Conversely, an "open" enum can 
be made "closed" (making default cases unnecessary), but a "closed" enum cannot 
be made "open".

That said, Clang now has an 'enum_extensibility' attribute that does take 
'open' or 'closed' as an argument.

On Matthew's thread, a few other possible names came up, though mostly only for 
the "closed" case:

- 'final': has the right meaning abstractly, but again it behaves differently 
than 'final' on a class, which is a restriction on code elsewhere in the same 
module.
- 'locked': reasonable, but not a standard term, and could get confused with 
the concurrency concept
- 'exhaustive': matches how we've been explaining it (with an "exhaustive 
switch"), but it's not exactly the enum that's exhaustive, and it's a long 
keyword to actually write in source.

- 'extensible': matches the Clang attribute, but also long


I don't have better names than "open" and "closed", so I'll continue using them 
below even though I avoided them above. But I would really like to find some.


Proposal

Just to have something to work off of, I propose the following:

1. All enums (NS_ENUMs) imported from Objective-C are "open" unless they are 
declared "non-open" in some way (likely using the enum_extensibility attribute 
mentioned above).
2. All public Swift enums in modules compiled "with resilience" (still to be 
designed) have the option to be either "open" or "closed". This only applies to 
libraries not distributed with an app, where binary compatibility is a concern.
3. All public Swift enums in modules compiled from source have the option to be 
either "open" or "closed".
4. In Swift 5 mode, a public enum should be required to declare if it is "open" 
or "closed", so that it's a conscious decision on the part of the library 
author. (I'm assuming we'll have a "Swift 4 compatibility mode" next year that 
would leave unannotated enums as "closed".)
5. None of this affects non-public enums.

(4) is the controversial one, I expect. "Open" enums are by far the common case 
in Apple's frameworks, but that may be less true in Swift.


Why now?

Source compatibility was a big issue in Swift 4, and will continue to be an 
important requirement going into Swift 5. But this also has an impact on the 
ABI: if an enum is "closed", it can be accessed more efficiently by a client. 
We don't have to do this before ABI stability—we could access all enums the 
slow way if the library cares about binary compatibility, and add another 
attribute for this distinction later—but it would be nice™ (an easy model for 
developers to understand) if "open" vs. "closed" was also the primary 
distinction between "indirect access" vs. "direct access".

I've written quite enough at this point. Looking forward to feedback!
Jordan
_______________________________________________
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

Reply via email to