[swift-evolution] Handling unknown cases in enums [RE: SE-0192]

Vladimir.S svabox at gmail.com
Tue Jan 9 14:27:07 CST 2018


Hi Chris, thank you for the new idea!

FWIW, after first reading, it looks like more elegant solution than "unknown case" in initial proposal. Swift's enums 
deserve powerful and flexible solution!

I really like the syntax of

switch val {
case .one: ...
case .two: ...
case #unknown: ...
}

instead of

switch val {
case .one: ...
case .two: ...
unknown case: ...
}

Also the possibility to use the #unknown in pattern matching is awesome.

Please find some comments/questions inline:

On 09.01.2018 9:54, Chris Lattner via swift-evolution wrote:
> The mega-thread about SE-0192 is a bit large, and I’d like to talk about one specific point.  In the review 
> conversation, there has been significant objection to the idea of requiring a ‘default’ for switches over enums that are 
> non-exhaustive.
> 
> This whole discussion is happening because the ABI stability work is introducing a new concept to enums - invisible 
> members/inhabitants (and making them reasonably prominent).  A closely related feature is that may come up in the future 
> is "private cases”.  Private cases are orthogonal to API evolution and may even occur on one that is defined to be 
> exhaustive.
> 
> Private cases and non-exhaustive enums affect the enum in the same way: they say that the enum can have values that a 
> client does not know about.  Swift requires switches to process *all* of the dynamically possible values, which is why 
> the original proposal started out with the simplest possible solution: just require ‘default' when processing the cases.
> 
> 
> *The problems with “unknown case:”*
> 
> The popular solution to this probably is currently being pitched as a change to the proposal 
> (https://github.com/apple/swift-evolution/pull/777) which introduces a new concept “unknown case” as a near-alias for 
> ‘default’:
> https://github.com/jrose-apple/swift-evolution/blob/60d8698d7cde2e1824789b952558bade541415f1/proposals/0192-non-exhaustive-enums.md#unknown-case
> 
> In short, I think this is the wrong way to solve the problem.  I have several concerns with this:
> 
> 1) Unlike in C, switch is Swift is a general pattern matching facility - not a way of processing integers.  It supports 
> recursive patterns and values, and enums are not necessarily at the top-level of the pattern.   
> https://github.com/apple/swift/blob/master/docs/PatternMatching.rst is a document from early evolution of Swift but 
> contains a good general introduction to this.
> 
> 2) Swift also has other facilities for pattern matching, including ‘if case’.  Making switch inconsistent with them is 
> not great.
> 
> 3) As pitched, “unknown case” will match *known* cases too, which is (in my opinion :-) oxymoronic.

Are you saying here about situation, when new case is added in non-frozen enum, but we are compiling the old code with 
absent new case but with "unknown case" in switch? As stated in proposal, in this case we'll have a warning exactly for 
source compatibility reason: "..unknown case matches any value. ... the compiler will produce a warning if all known 
elements of the enum have not already been matched. This is a warning rather than an error so that adding new elements 
to the enum remains a source-compatible change." Do you suggest to change that warning into error?

Or you are saying about another situation when “unknown case” will match known cases too in the initial proposal ?


> 
> 4) “unknown case:” changes the basic swift grammar (it isn’t just a modifier on case) because case *requires* a pattern. 
>   A better spelling would be “unknown default:” which is closer to the semantic provided anyway.
> 
> 5) It is entirely reasonable (though rare in practice) to want to handle default and unknown cases in the same switch.
> 

Are you suggesting to have this?:

switch val {
case .one: ...
case .two: ...
case #unknown: ...
default: ..
}

So, all new cases, introduced in external module after compilation of that code will fall into "case #unknown: ..." 
bucket, but all "known"(at the moment of compilation) cases but not .one/.two - into "default:..." ? Personally I like 
that, so if I need this - I will be able to express this, but that rule seems like complicated to understand.

But this can be really helpful in situation, when you *at compilation time" thinks that you are not interested in 
anything but .one/.two cases of that enum, but in case of new values - you want to show(for example) a warning for app user.

> 
> For all the above reasons, ‘unknown case:' becomes a weird wart put on the side of switch/case, not something that fits 
> in naturally with the rest of Swift.
> 
> 
> *Alternative proposal:*
> 
> Instead of introducing a new modifier on case/switch, lets just introduce a new pattern matching operator that *matches 
> unknown cases*, called “#unknown” or “.#unknown” or something (I’m not wed to the syntax, better suggestions welcome :).
> 
> In the simple case most people are talking about, instead of writing “unknown case:” you’d write “case #unknown:” which 
> isn’t much different.  The nice thing about this is that #unknown slots directly into our pattern matching system.  Here 
> is a weird example:
> 
> switch someIntEnumTuple {
> case (1, .X):   … matches one combination of int and tuple...
> case (2, .Y):   … matches another combination of int and tuple...
> case (_, #unknown): …  matches any int and any unknown enum case ...
> case default:  … matches anything ...
> }
> 
> Furthermore, if you have a switch that enumerates all of the known cases and use #unknown, then it falls out of the 
> model that new cases (e.g. due to an SDK upgrade or an updated source package) produces the existing build error.  As 
> with the original proposal, you can always choose to use “default:” instead of “case #unknown:” if you don’t like that 
> behavior.

Do I understand correctly, that you suggest the same behavior as in original(updated) proposal:
* if we have a switch on non-frozen enum value, and enumerated some cases, and have "case #unknown" - we'll have a 
warning(error?) if (at the moment of compilation) not all known enums are processed in "switch" ?
Just to be sure.

Btw, should this be valid? :
switch val {
   case #unknown:... // I'm only interested if new values were added to that external enum
   default: ... // as I understand, "default" will be required here
}

> 
> Of course, if you have an exhaustive enum (e.g. one defined in your own module or explicitly marked as such) then 
> #unknown matches nothing, so we should warn about it being pointless.
> 

I have some concerns(applied to the initial proposal also) about the warnings, that depend on how the enum was 
imported(same module / other module).

Is the warning really needed in this case? Yes, we have #unknown case, we understand that enum can(and probably will 
not) be changed, so the code probably will never be called.
Probably we want to use the same source file with this "switch" both in separate module and in the same module. Then 
we'll have unnecessary warning(can't silence it) when compiling in the same module.
Or we just want to write "general"("common") switch code not thinking about how enum will be imported later, so always 
write #unknown to complete the logic. No? At least probably we then need a way to silence the warning to keep "case 
#unknown" in the source in the same module with enum declaration.

Also, regarding the possible "private cases" in enum. IIUC, in case of private cases we'll need "case #unknown" in 
switch even for enums declared in the same module. Otherwise how we'll separate "default"(known "public" cases we are 
aware of but not interested in) and "private cases of that enum we know nothing about". But then how to separate "future 
public cases" and "private cases of enum" in switch?
Or some "case #private" will work better here? But then why "case #unknown" and not "case #future" ?
So, may we want to have this? :
switch extern_enum_val {
   case .one: ..
   case .two: ..

   case #private: .. // decision for some private value of enum
   case #unknown: .. // decision for some "new"(future) public value of enum
   default: .. // decision for known *at compile* public values other than listed above
}
Or probably I understand the concept of "private cases" wrong, then sorry.
(Sorry for such a "flow of questions" :-) Just want to understand the proposed solutions better)

> 
> This addresses my concerns above:
> 
> 1) This fits into patterns in recursive positions, and slots directly into the existing grammar for patterns.  It would 
> be a very simple extension to the compiler instead of a special case added to switch/case.
> 
> 2) Because it slots into the pattern grammar, it works directly with 'if case’ and the other pattern matching stuff.
> 
> 3) Doesn’t match known cases.
> 
> 4) Doesn’t change the case grammar, it just adds a new pattern terminal production.
> 
> 5) Allows weird cases like the example above.
> 
> 
> All that said, the #unknown spelling isn’t great, but I’m sure we can find something else nice.
> 
> Thoughts?
> 

Personally I like #unknown - similar like other stuff that depends on compiler magic and '#' clearly separates 
'#unknown' from other 'usual' cases. (Actually I like #future more, especially if we are going to have something like 
#private to clearly separate unknown future public cases, and unknown private cases)

Vladimir.

> -Chris
> 
> 
> 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
> 


More information about the swift-evolution mailing list