And added below is an Option 7.
> On Aug 21, 2020, at 8:32 PM, Guy Steele <[email protected]> wrote:
>
>
>
>> On Aug 21, 2020, at 4:18 PM, Brian Goetz <[email protected]
>> <mailto:[email protected]>> 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.