Hi Jordan, The proposal looks very reasonable to me.
I don’t have too strong an opinion on this topic, but it occurred to me that your discussion of `future` left out one possible design approach. We could restrict code in the `future` clause to be `break` or `fatalError()`. One of these is probably what most people would do any way and neither really requires testing. As you noted, it will be possible to have untestable code in a `default` clause which is probably what people asking for `future` will have in the relevant switch statements. Supporting `future` does seem like a nice way to allow libraries to add cases without a breaking change while allowing users to opt-in to a breaking change when that happens. It’s a nice compromise that doesn’t appear to harm anyone. The main argument against it is: what are the use cases? I haven’t thought enough about it to answer that question. I would challenge people asking for `future` to try to provide some concrete examples, probably referencing enums in Apple frameworks. Maybe if sufficient motivation can be demonstrated we should reconsider the more limited form of `future` that doesn’t involve untestable code. On the other hand, `future` is something that can always be added later. As I said, I don’t have a strong opinion about this either way at the moment. Matthew > On Sep 5, 2017, at 7:19 PM, Jordan Rose <jordan_r...@apple.com> wrote: > > I've taken everyone's feedback into consideration and written this up as a > proposal: > https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md > > <https://github.com/jrose-apple/swift-evolution/blob/non-exhaustive-enums/proposals/nnnn-non-exhaustive-enums.md>. > The next step is working on an implementation, but if people have further > pre-review comments I'd be happy to hear them. > > Jordan > > >> On Aug 8, 2017, at 15:27, Jordan Rose <jordan_r...@apple.com >> <mailto:jordan_r...@apple.com>> 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 >> <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 swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution