Hi Jordan,

let's say I'm writing my custom number formatter and I switch over 
NSNumberFormatterStyle (NumberFormatter.Style in Swift) - the question always 
is what to do with the default case - it's a value that I am not 
programmatically counting with. I would personally just put in fatalError with 
a description that it was passed an unhandled style. Listing all enums in 
Foundation, I can see using most of them this way.

I personally have most of my switches exhaustive, mainly for the sake of being 
warned/error'ed when a new case is introduced - I've just done a quick search 
through my projects and I use default: usually for switching over non-enums 
(strings, object matching, ints, ...).

Maybe I'm in the minority here... Seemed like a good practice to me - usually 
the enum doesn't have but a few items on the list and you usually don't handle 
just 1-2 cases in your switch, which makes the default label save you 1-2 lines 
of code that can save you from unnnecessarily crashing during runtime...

> On Aug 10, 2017, at 1:57 AM, Jordan Rose <jordan_r...@apple.com> wrote:
> 
> Hi, Charlie. This is fair—if you're switching over an open enum at all, 
> presumably you have a reason for doing so and therefore might want to handle 
> all known cases every time you update your SDK. However, I think in practice 
> that's going to be rare—do you have examples of exhaustive switches on SDK 
> enums that exist in your own app?
> 
> (There's an additional piece about how to handle cases with different 
> availability—there's nowhere obvious to write the #available.)
> 
> I suspect marking SDK enums "closed" will be much easier than nullability, 
> simply because there are so few of them. Here's some data to that effect: out 
> of all  60 or so NS_ENUMs in Foundation, only 6 of them are ones I would 
> definitely mark "closed":
> 
> - NSComparisonResult
> - NSKeyValueChange / NSKeyValueSetMutationKind
> - NSRectEdge
> - NSURLRelationship
> - maybe NSCalculationError
> 
> There are a few more, like NSURLHandleStatus, where I could see someone 
> wanting to exhaustively switch as well, but the main point is that there is a 
> clear default for public enums, at least in Objective-C, and that even in a 
> large framework it's not too hard to look at all of them.
> 
> (Note that NSComparisonResult is technically not part of Foundation; it lives 
> in the ObjectiveC module as /usr/include/objc/NSObject.h.)
> 
> Jordan
> 
> 
>> On Aug 8, 2017, at 21:53, Charlie Monroe <char...@charliemonroe.net 
>> <mailto:char...@charliemonroe.net>> wrote:
>> 
>> While I agree with the entire idea and would actually use behavior like this 
>> in a few instances, I feel that in most cases, you would simply put 
>> 
>> default:
>>      fatalError()
>> 
>> The huge downside of this is that you no longer get warned by the compiler 
>> that you are missing a case that was added - a common thing I personally do 
>> (and I have a feeling I'm not alone) - add an enum case, build the app, see 
>> what broke and fix it - as you get warnings/errors about the switch not 
>> being exhaustive. You find this out during runtime (if you're lucky), 
>> otherwise your end user.
>> 
>> As you've noted all enums from ObjC would need to be marked with an 
>> annotation marking if they are closed - which given the way nullability is 
>> still missing in many frameworks out there, I think would take years.
>> 
>> I'd personally expand this proposal by introducing switch! (with the 
>> exclamation mark) which would allow to treat open enums as closed. Example:
>> 
>> // Imported from ObjC
>> open enum NSAlert.Style { ... }
>> 
>> switch! alert.style {
>> case .warning:
>>      // ...
>> case .informational:
>>      // ...
>> case .critical:
>>      // ...
>> }
>> 
>> The force-switch would implicitely create the default label crashing, 
>> logging the rawValue of the enum.
>> 
>> Thoughts?
>> 
>>> On Aug 9, 2017, at 12:28 AM, Jordan Rose via swift-evolution 
>>> <swift-evolution@swift.org <mailto: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 
>>> <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 <mailto:swift-evolution@swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>>> <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