And added below is an Option 7. > On Aug 21, 2020, at 8:32 PM, Guy Steele <guy.ste...@oracle.com> wrote: > > > >> On Aug 21, 2020, at 4:18 PM, Brian Goetz <brian.go...@oracle.com >> <mailto:brian.go...@oracle.com>> wrote: >> >> >> >> On 8/21/2020 11:14 AM, Brian Goetz wrote: >>> >>> Next up (separate topic): letting statement switches opt into totality. >>> >> >> Assuming the discussion on Exhaustiveness is good, let's talk about >> totality. >> >> Expression switches must be total; we totalize them by throwing when we >> encounter any residue, even though we only require that the set of cases in >> the switch be optimistically total. Residue includes: >> >> - `null` switch targets in String, Enum, and primitive box switches only; >> - novel values in enum switches without a total case clause; >> - novel subtypes in switches on sealed types without a total case clause; >> - when an optimistically total subchain of deconstruction pattern cases >> wraps a residue value (e.g., D(null) or D(novel)) >> >> What about statement switches? Right now, any residue for a statement >> switch without a total case clause will just be silently ignored (because >> statement switches need not be total.) >> >> What we would like is a way to say "this switch is total, please type check >> it for me as such, and insert any needed residue-catching cases." I think >> this is a job for `default`. >> >> Now that we've got some clarity that switches _don't_ throw on null, but >> instead it is as if string/enum/box switches have an implicit `case null` >> when no explicit one is present, we can define `default`, once again, to be >> total (and not just weakly total.) So in: >> >> switch (object) { >> case "foo": >> case Box(Frog fs): >> default: ... >> } >> >> a `null` just falls into `default` just like anything else that is not the >> string "foo" or a box of frogs ("let the nulls flow"). Default would have >> to come last (except in legacy switches, where a legacy switch has one of >> the distinguished target types and all constant case labels.) >> >> What if we want to destructure too? Well, add a pattern: >> >> switch (object) { >> case "foo": >> case Box(Frog fs): >> default Object o: ... >> } >> >> This would additionally assert that the following pattern is total, >> otherwise a compilation error ensues. (Note, though, that this is entirely >> about `switch`, not patterns. The semantics of the pattern is unchanged, >> and I do not believe that sprinkling `default` into nested patterns to shout >> "TOTALITY HERE, I MEAN IT" carries its weight.) >> >> This seems a better job to give default in this new world; anything not >> previously matched, where we retcon the current null behavior as being only >> about string, enum, or boxes. >> >> This leaves us with only one hole, which is: suppose I have an >> _optimistically total_ statement switch. Users might like to (a) assert >> the switch is total, and get the concomitant type checking, and (b) get >> residue ejection for free. Of the two, though, A is much more important >> than B, but we'll take B when we can get it. Perhaps, if the target of a >> switch is a sealed type, we can interpret: >> >> switch (shape) { >> case Rect r: ... >> default Circle c: ... >> } >> >> as meaning that `Circle c` _closes_ the switch to make it total, and engages >> the totality checking to ensure this is true. So, `default P` would mean >> either: >> >> - P is total, or >> - P is not total, but taken with the other cases, makes the switch >> optimistically total >> >> and in the latter case, would engage the residue-detection-and-ejection >> machinery. >> >> This might be stretching it a tad too far, but I like that we can given >> `default` useful new jobs to do in `switch` rather than just giving him a >> gold watch. > > This is a pretty good story, but I am sufficiently distressed over the > asymmetry of having to treat specially the last one of several otherwise > completely symmetric and equal cases: > > switch (color) { > case Red: … > case Green: … > default Blue: … > } > > when I would much rather see > > switch (color) { > case Red: … > case Green: … > case Blue: … > } > > that I am going to explore several other design options, some of them more > obviously terrible than others, in hopes of prompting someone else to have a > brilliant idea. > > First of all, let me note that after Brian’s detailed analysis about the > treatment of `null`, the only real difficulty we face is compatibility with > legacy switches on enum types. We missed an opportunity when enum was first > introduced. I really hate to recommend an incompatible change to the > language, but this message is just brainstorming, so: > > Option 1: If the type of the switch expression is an enum or a sealed type, > then it is a static error if the patterns are not at least optimistically > total. **This would be an incompatible change with respect to existing > switches on enum types.** > > Option 2: If the type of the switch expression is a sealed type, then it is a > static error if the patterns are not at least optimistically total. This > treats enums and sealed types differently, but is compatible (as are all the > other options I will list below). > > Option 3: If the type of the switch expression is a sealed type, then it is a > static error if the patterns are not at least optimistically total. You can > get the benefit of this feature when switching on an enum type by adding the > keyword “sealed” to the declaration of the enum type. > > enum Color { RED, GREEN } > Color x; > switch (x) { RED: … } // Okay > > sealed enum Color { RED, GREEN } > Color x; > switch (x) { RED: … } // static error: cases are not > optimistically total > > Option 4: If the type of the switch expression is a sealed type, then it is a > static error if the patterns are not at least optimistically total. You can > get the benefit of this feature when switching on an enum type by adding the > keyword “enum” to the switch statement. > > enum Color { RED, GREEN } > Color x; > switch (x) { RED: … } // Okay > > enum Color { RED, GREEN } > Color x; > switch enum (x) { RED: … } // static error: cases are not > optimistically total > > Option 5: Expression switches must be total. So if you want a statement > switch but want it to be total, convert it to an expression switch by writing > “(void)” in front of it (and add a semicolon at the end). > > enum Color { RED, GREEN } > Color x; > switch (x) { RED: … } // Okay > > enum Color { RED, GREEN } > Color x; > (void) switch (x) { RED: … }; // static error: cases are not > optimistically total > > (Yeah, I have glossed over a number of details here.) > > Option 6: The classic idiom for switching on a enum type looks like this > example taken from the JLS: > > switch (c) { > case PENNY: return CoinColor.COPPER; > case NICKEL: return CoinColor.NICKEL; > case DIME: case QUARTER: return CoinColor.SILVER; > default: throw new AssertionError("Unknown coin: " + c); > } > > The only really annoying thing about this is having to write (and read) the > boilerplate code for constructing the error to be thrown. So how about this > abbreviation: > > switch (c) { > case PENNY: return CoinColor.COPPER; > case NICKEL: return CoinColor.NICKEL; > case DIME: case QUARTER: return CoinColor.SILVER; > default throw; > } > > The meaning of “default throw;” is that it is a static error if the case > patterns are not optimistically total (and it reminds you that you will get > some synthetic default cases that will throw an error if something goes > wrong).
[Forgive me; I have realized that I omitted the keyword “case” in all the case clauses in the previous examples.] Option 7: If the switch expression is a cast expression, then it is a static error if it is a static error if the patterns are not at least optimistically total. enum Color { RED, GREEN } Color x; switch (x) { case RED: … } // Okay enum Color { RED, GREEN } Color x; switch ((Color)x) { case RED: … } // static error: cases are not optimistically total This idea works for _any_ type, not just sealed or enum types. If you are switching on an int, then switch(v) { case 1: … case 2: ... } is fine, but switch((int)v) { case 1: … case 2: ... } will be a static error, and the only way to avoid such a static error will be to include a default clause or the equivalent (such as “case var z”). I hate to break =(or even bend) the pure compositionality of expression syntax, but this approach its likely to be backward compatible in practice and is a fairly clear indication of what the programmer intends.