[swift-evolution] Enums and Source Compatibility

Vladimir.S svabox at gmail.com
Tue Aug 15 13:41:37 CDT 2017


On 12.08.2017 1:37, Jordan Rose wrote:
> 
> 
>> On Aug 11, 2017, at 02:59, Vladimir.S <svabox at gmail.com <mailto:svabox at gmail.com>> 
>> wrote:
>>
>> On 11.08.2017 2:37, Jordan Rose wrote:
>>> Both you and Vladimir are bringing up this point, with Vladimir explicitly 
>>> suggesting a "future" case that's different from "default". Again, the pushback we 
>>> get here is that the "future" case is untestable…but maybe that's still an option 
>>> worth having.
>>
>> I wonder, how the 'default' in exhaustive switch on open enum is testable?
>>
>> I mean, let's say we have such enum in one of the frameworks:
>>
>> open enum MyOpenEnum {
>>  case one
>>  case two
>> }
>>
>> , then in our code:
>>
>> switch myOpenEnumInstance {
>>  case .one : ...
>>  case .two : ...
>>  default : ... // how this can be tested?
>> }
>>
>> I just strongly feel that be able to keep switch exhaustive at the moment of 
>> compilation - is critical requirement in some cases, when it is very important to 
>> not forget to process some cases. With just 'default' in switch for open enum - we 
>> are loosing this compiler's help. This is why 'future' case is required for open enums.
>>
>> Also, if I understand correctly, we are going to have most of all extern(imported) 
>> enums as 'open'. So, we are loosing the feature to receive a help for exhaustive 
>> switch from compiler for most of such enums.
> 
> That's true, but I don't know what to do about it. My hypothesis, again, is that this 
> won't be common in practice; so far Charlie's been the only one to provide a 
> real-world example of when this is useful.
> 

So, do you want to say that we usually will not have open enums which we want to 
switch exhaustive? Probably I can't understand exactly this point.
Seems like you are saying about enums in Foundation/SDK, while I'm thinking about any 
given framework/module, including compiled from 100% Swift code, even made by 
me/colleague.

It will be very interesting to check the number of exhaustive switches in open source 
Swift projects regarding enums imported from other frameworks. So, this can tell us 
if developers often/rarely decided to check each case in external enum, declared in 
some framework. Probably someone have such information?

My request for 'future' block in switch for open enum based on belief that it can be 
also my(as framework's user) decision if *I* want to keep the switch exhaustive in my 
code in some situation. Framework's author just can't foresee all the possible 
situations and requirements of my code. For some reason I still can't accept that 
this should be just decision of framework's author.
I do expect that the lack of 'future' block for open enums decrease the quality of 
Swift code in framework user's code as keep the switch exhaustive will be hard thing 
without compiler's help.
"Cases could be added in this open enum in future" and "I, as a framework author, 
prevents you from having comiller's help to keep switch exhaustive on this enum" 
should not be the same things.


Thinking further, given source compatibility requirement, can we require a change in 
consumer's code (in exhaustive switches) by changing what 'public enum {..}' means?

I mean, currently public enum is treated as 'closed', so you have to be exhaustive or 
have 'default' block. If we make 'usual' declaration of public enum means 'open' - 
we'll break some consumer's code - as starting from this point they have to contain 
'default'/'future' block in exhaustive switch for the same enum declaration.
So, probably, because of this, current 'usual' declaration of public enum should 
means 'closed' enum, just like now. So, all *current* code will just compile. And 
this will provide a space for optimizations for public enums declared with current 
syntax.

Then, in this case, for open enums we need a special marker.
FWIW Right now I see only 3 good candidates:
1. 'open enum'
2. 'public(future) enum'
3. 'public enum MyEnum {case..; case..; future}'
All have pros and cons, while (2) is more explicit and less confusing for me.

But then I was thinking about the Error enums. Most likely it will contain more cases 
in future releases, but by mistake framework's developer exported it without 'open' 
marker, so any new cases will lead to crash in consumer's code.

What if we consider such solution:
* by default 'public enum' means 'closed' enum and we fully support the source 
compatibility requirement
* Swift generates a warning for such enum, something like "Published enum will be 
'closed', but you should consider if you really want this as new cases will crash 
consumer's code. Please use ... to explicitly express your intention"
* developer then can think and decide what is a 'kind' of this enum and use some 
explicit marker to silence the warning and to express intention. For example:
   * 'public(closed)'(or 'public(final)' or 'public(sealed)') for 'closed' enums, and 
'public(future)' for 'open' enums
   OR
   * public enum MyError: Error { case..; case..; final} (for closed) and
     public enum MyError: Error { case..; case..; future} (for open)
* 'switch' for open enum should contains 'default' or 'future' block. In case of 
'future' block, such switch must me exhaustive at the moment of _compilation_.

IMO clear intention and thoughtful decision regarding published enum worth some 
additional syntax for *public* enums.

Yes, alternative strategy could be make 'usual' declaration of public enum means 
'open' and mark only 'closed' enums with some marker. So, "by default" framework's 
author can be free to append new cases in enums without breaking user's code 
written/compiled with older version of framework.
BUT this leads to breaking changes in current user's code(have to append 
'default'/'future' in exhaustive switches) and in case we will not have 'future' 
block - developers will lose current quality of compiler's support.
In case we can introduce such change, and in case we'll have 'future' block in switch 
- I(FWIW) believe this also will be good solution, easy to understand but flexible, 
which keeps current guarantees of compiler's help.


I hope the text above makes any sense :-)

Vladimir.

> 
>> Moreover, shouldn't we just say that enums, that we have no Swift sources for at 
>> the moment of compilation - should *always* be treated as 'open'? If we compile our 
>> 'switch' together with the source of switched enum - such enum can not be changed 
>> in the future.
>> But, if enum is coming from other framework - we have no any control over it, and 
>> even author of framework IMO can't be sure in most cases that enum will not be 
>> extended in future, and probably we even should not ask author of framework to 
>> consider its enum closed, as most likely one can't foresee for sure.
>>
>> Wouldn't this be a simpler and more robust model to think about enums in Swift?
>> So you know, that *any* enum coming from framework(not from source) - can be 
>> changed in future, and so you have to process this case explicitly. In this case we 
>> don't need to mark enum as 'open' or 'closed' at all, but for some rare cases, when 
>> author of framework *really sure* enum can not be changed in future(and future 
>> change in enum will break all the code depending on it), we can introduce some 
>> 'final' marker(or @exhaustive directive) to help compiler's optimizations.
> 
> Again, some enums really do want to be exhaustive: Foundation.ComparisonResult, 
> Swift.Optional, and in general anything the framework owner really /does/ want people 
> to exhaustively switch over. These aren't just optimization concerns because they 
> affect how people are expected to use the type. I think this all just means that 
> you're on the side that "open" should be the default.
> 
> 
>>
>> Btw, is open enum is allowed to change the type of associated value for some cases 
>> or even enum's raw type? I.e. what changes in open enum will lead to crash in our 
>> code and which will just be processed in 'default'/'future' block in switch?
> 
> Nope, that's not planned to be allowed. That would break source compatibility outside 
> of just switch—it would also affect `if case` as well as the /creation/ of an enum 
> with that case.
> 
> The last time I thought about this, I came up with this list of things we want to 
> allow in "open" enums:
> 
> • Adding a new case.
> • Reordering existing cases is a "binary-compatible source-breaking change". In 
> particular, if an enum is RawRepresentable, changing the raw representations of cases 
> may break existing clients who use them for serialization.
> • Adding a raw type to an enum that does not have one.
> • Removing a non-public, non-versioned case (if we ever have such a thing).
> • Adding any other members.
> • Removing any non-public, non-versioned members.
> • Adding a new protocol conformance (with proper annotations).
> • Removing conformances to non-public protocols.
> 
> We're now questioning whether reordering should be allowed at all for implementation 
> reasons, but other than that this list should still be accurate.
> 
>>
>> Vladimir. (P.S. Sorry for long reply)
> 
> Thanks for thinking about this in detail; better to get the problems out in the open 
> now before I write up a formal proposal!
> 
> Jordan
> 


More information about the swift-evolution mailing list