> On 10 Aug 2017, at 02:42, Jordan Rose <[email protected]> wrote: > > :-) As you've all noted, there are some conflicting concerns for the default: > > - Source compatibility: the existing behavior for an unannotated enum is > "closed". > - Intuition: if you show someone an enum without an explicit annotation, > they'll probably expect they can switch over it. (I'm going to say this is > why Zach calls it a "sensible default".) > - Consistency: switches on an enum in the same module can always be > exhaustive, so having it be different across modules is a bit annoying. (But > 'public' already acts like this.) > > vs. > > - Library evolution: the default should promise less, so that you have the > opportunity to change it. > - Flexibility: you can emulate an exhaustive switch with a non-exhaustive > switch using fatalError, but not the other way around. > > All of this is why I suggested it be an explicit annotation in either > direction, but Matthew brought up the "keyword soup" problem—if you have to > write (say) "public finite enum" and "public infinite enum", but would never > write "private finite enum" or "private infinite enum", something is > redundant here. Still, I'm uncomfortable with the default case being the one > that constrains library authors, so at least for binary frameworks (those > compiled "with resilience") I would want that to be explicit. That brings us > to one more concern: how different should binary frameworks be from source > frameworks?
In terms of intuition and consistency, I think we should really try to learn from the simplicity of public/open: * When internal, classes are sub-classable by default for convenience, but can be closed with the final keyword * When public, classes are closed to sub-classing for safety, but can be opened up with the open keyword (which implies public). If we try to mirror this behaviour (the keywords are just suggestions, not important): * When internal, enums are exhaustive by default for convenience, but can be opened-up with the partial keyword * When public, enums are non-exhaustive by default for safety, but can be made exhaustive with the exhaustive keyword (which implies public). David. > Jordan > > > >> On Aug 9, 2017, at 08:19, Zach Waldowski via swift-evolution >> <[email protected] <mailto:[email protected]>> wrote: >> >> I disagree. Closed is indeed the stronger guarantee, but APIs are designed >> differently in Swift; closed is a sensible default. We shouldn’t need to >> define new keywords and increase the surface area of the language for >> something that has verisimilitude with the existing open syntax. >> >> Sincerely, >> Zachary Waldowski >> [email protected] <mailto:[email protected]> >> >> >> On Wed, Aug 9, 2017, at 06:23 AM, David Hart via swift-evolution wrote: >>> >>> >>> On 9 Aug 2017, at 09:21, Adrian Zubarev via swift-evolution >>> <[email protected] <mailto:[email protected]>> wrote: >>>> Hi Jordan, is that only me or haven't you metioned the default should be >>>> applied to all new enums? Personally I'd say that 'closed' should be the >>>> default and the 'open' enum would require an extra keyword. >>> >>> I think it should definitely be the other way round for public enums >>> because closed is the stronger guarantee. Final is the default for classes >>> because open is the stronger guarantee. That’s probably why we should not >>> use the same keywords. >>> >>>> Now about the keyword itself. Here are two keywords that IMHO nail their >>>> behavior down to the point: >>>> >>>> finite enum A {} - so to say a closed enum (default) >>>> infinite enum B {} - so to say an open enum (requires default case in a >>>> switch statement) >>>> >>>> If you think the default should be the other way around, than feel free to >>>> switch that. 'finite' also implies that the enum connot ever be extended >>>> with more cases (to become infinite), which was also mentioned in your >>>> email. >>>> >>>> -- >>>> Adrian Zubarev >>>> Sent with Airmail >>>> Am 9. August 2017 um 00:27:53, Jordan Rose via swift-evolution >>>> ([email protected] <mailto:[email protected]>) schrieb: >>>> >>>>> >>>>> >>>>> >>>>> 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 >>>>> <http://jrose-apple.github.io/swift-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 >>>>> [email protected] <mailto:[email protected]> >>>>> https://lists.swift.org/mailman/listinfo/swift-evolution >>>>> <https://lists.swift.org/mailman/listinfo/swift-evolution> >>>>> >>>> _______________________________________________ >>>> swift-evolution mailing list >>>> [email protected] <mailto:[email protected]> >>>> https://lists.swift.org/mailman/listinfo/swift-evolution >>>> <https://lists.swift.org/mailman/listinfo/swift-evolution> >>> _______________________________________________ >>> swift-evolution mailing list >>> [email protected] <mailto:[email protected]> >>> https://lists.swift.org/mailman/listinfo/swift-evolution >>> <https://lists.swift.org/mailman/listinfo/swift-evolution> >> >> _______________________________________________ >> swift-evolution mailing list >> [email protected] <mailto:[email protected]> >> https://lists.swift.org/mailman/listinfo/swift-evolution >
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
