> De: "Guy Steele" <guy.ste...@oracle.com> > À: "Brian Goetz" <brian.go...@oracle.com> > Cc: "Remi Forax" <fo...@univ-mlv.fr>, "Tagir Valeev" <amae...@gmail.com>, > "amber-spec-experts" <amber-spec-experts@openjdk.java.net> > Envoyé: Samedi 22 Août 2020 02:32:13 > Objet: Re: [pattern-switch] Totality
>> On Aug 21, 2020, at 4:18 PM, Brian Goetz < [ mailto:brian.go...@oracle.com | >> 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.) I've already written a code organically doing mostly that, transforming a statement switch to an expression switch to be sure it did not compile when i will add more enum constants, var _ = switch(x) { case RED: ... yield false; case GREEN: ... yield false }; > 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). If we go down to the route of saying that switch on enum, string and box are special because null hostile, why not go a step further and say that, apart those switches and the switch one primitive types, all other switches should be total, so obviously an expression switch should be total but a statement switch should be total too. And now we only need to solve the problem of enums inside a statement switch, here i disagree with Brian that it's a job for "default", as a developer i want the compiler to emit an error at compile time not at runtime. I wonder if like Option 1 we can not bully our way out by first raising a warning if the statement switch is not optimistically total (IDEs already does that but ask for a default) and adds an ICCE automatically if the switch is total (it's a behavior incompatible change but it's for aligning the statement switch to the expression switch and i believe it will be fine in real life) then later convert that warning to an error like we want to do with wrapper type and ==. I also want to add that if we add things like guards, we may also want this kind of switches to be exhaustive, int i = ... switch(i) { case i where i > 0: ... } Rémi