[swift-evolution] Enums and Source Compatibility

Vladimir.S svabox at gmail.com
Thu Aug 10 10:11:01 CDT 2017


On 10.08.2017 3:58, Jordan Rose wrote:
> Hi, Vladimir. I think framing this as a consumer decision is the wrong place to 
> start. There are some enums that definitely make sense to be "closed", all the time, 
> with no additional annotations, including Foundation.NSComparisonResult and, well, 
> Swift.Optional. (Yes, Optional is special, and we could always handle it specially if 
> we needed to, but it would be /nice/ if it used the same logic as everything else.)
> 
> Beyond that, I think something like what you and Charlie describe (your 
> '@exhaustive', Charlie's 'switch!') could make sense, as a way to provide a 
> deterministic behavior while still getting compiler warnings for not being 
> exhaustive. (Probably the only supported behavior here would be trapping, since there 
> is no way to test such a scenario with the libraries you currently have.) But I'd 
> like to see a real-world example of exhaustively switching over an enum in the SDK 
> before designing a feature around this; I strongly suspect it's not something we need…
> 
> …/in the binary framework case/. It may still be interesting for source frameworks, 
> particularly C enums in those source frameworks.
> 

Hi Jordan. Thank you for comments. It seems like I'm missing something important.. 
Will only SDK framework contains open enums? Can't I wish to have an exhaustive 
switch in my own code regarding some open enum coming from some other's framework and 
to be sure I'm processing all the cases during the compilation time of my code with 
that framework?

I assume everybody(ok,most of us) thanks Swift for helping us keep switch exhaustive 
when we *need* this, so we will not forget to process new *known* cases, and I don't 
understand why this can be changed with 'open' enums.

Yes, for open enums we *have to* process 'future' cases in switch(even if it is 
exhaustive at the moment of compilation), but also for some open enums we *will* need 
a help from Swift compiler to keep our switch exhaustive for the moment of compilation.

And for such switch(exhaustive for known at compilation time cases of open enum), we 
probably should introduce some 'future' keyword:

switch openEnum {
   case .yes: ..
   case .no: ..
   future: ...  // this is for future cases only, all known cases should be processed
}

And you can use 'default' as usual, if you don't care of *any* other cases(including 
future cases) in this open enum:

switch openEnum {
   case .yes: ..
   case .no: ..
   default: ...
}

The 'future' or 'default' would be then required for switch on open enum instance.

Vladimir.

> Jordan
> 
> 
>> On Aug 9, 2017, at 09:57, Vladimir.S <svabox at gmail.com <mailto:svabox at gmail.com>> 
>> wrote:
>>
>> Sorry if I misunderstood the subject, but shouldn't this also be a *consumer* 
>> decision, when one wants to keep own switch exhaustive or it is OK to process all 
>> possible future cases in 'default'?
>>
>> If enum is 'closed' - nothing is changed, as I understand, we have the same rules 
>> for switch as currently.
>>
>> If 'external' enum, from other framework, was marked as 'closed' but was actually 
>> changed in new version of framework/module - our source code will fail to compile 
>> (because we need fix our switch cases) and our binary module(built with previous 
>> version of framework) will crash.
>>
>> With 'open' enums, depending on situations, I as a consumer of external framework, 
>> can decide that it is important to me, in my code, check *each* value of external 
>> enum in switch. If new enum case added/changed in external framework - my code must 
>> fail to compile and notify me that new case should be processed.
>> Once we added 'default' case in 'switch' in our code for 'open' enum - we lost 
>> compiler's support to keep our 'switch' exhaustive.
>>
>> But from other side, in other situation, I want to process all new cases for 'open' 
>> enum in 'default' branch, and so allow my source/compiled code to work with new 
>> version of framework(with added cases).
>>
>> So, it seems like in both situations we need to explicitly tell what is our decided 
>> behavior for new cases for 'open' enum in exhaustive switch : crash/fail to compile 
>> or process in 'default' block.
>>
>> What if in case of exhaustive switch on 'open' enum, compiler warns us saying "this 
>> is an exhaustive switch on 'open' enum, but cases can be added later. clarify what 
>> is your decided behaviour. by default your code will crash if new cases are added"
>> , then we can add 'default' block to process new future cases or somehow mark that 
>> switch as explicitly exhaustive... something like this:
>>
>> @exhaustive
>> switch openEnum {
>>  case .yes : ...
>>  case .no : ...
>> }
>>
>> What I'm missing?
>>
>> On 09.08.2017 1:27, Jordan Rose via swift-evolution 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 at swift.org <mailto:swift-evolution at swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
> 


More information about the swift-evolution mailing list