[pattern-switch] Totality
Guy Steele
guy.steele at oracle.com
Sat Aug 22 16:24:42 UTC 2020
And added below is an Option 7.
> On Aug 21, 2020, at 8:32 PM, Guy Steele <guy.steele at oracle.com> wrote:
>
>
>
>> On Aug 21, 2020, at 4:18 PM, Brian Goetz <brian.goetz at oracle.com <mailto:brian.goetz at 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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20200822/c6689631/attachment-0001.htm>
More information about the amber-spec-experts
mailing list