[External] : Re: Final variable initialization problem with exhaustive switch

Brian Goetz brian.goetz at oracle.com
Wed Dec 1 16:29:56 UTC 2021


> The title of this thread is probably just the tip of the iceberg at 
> this point..

Well, maybe, in a sense: that this really is the glass 5% empty.

Originally all switches were partial, and therefore got no benefit of 
totality checking.  (Thanks, C.)

When we added expression switches, totality was a forced move: 
expressions are total.  So we had a seam; expression switches total, 
statement switches partial.  (There was much sound and fury about "you 
should make a new switch construct", but that would just be a different 
sort of seam, and its new-ness wouldn't last forever either, so we chose 
the lesser of evils.)

When we added patterns to switches, this created a new kind of potential 
seam; "old" switches (those on primitives, strings, and enums, with only 
constant labels) and "new" ones (those with patterns.)  Because these 
new switches were much more powerful (or would be, when the pattern 
matching arc delivers further), this shifts the design center for the 
feature, making it useful in places where it wasn't before (especially 
with the addition of sealed types).  Partial-by-default started to look 
like a bad default.  So we took the bold move of making these new 
switches (those with patterns) total as well.  (You're not complaining 
about this, but in a mirror-universe, Bearded Dimitris is complaining 
"why are you making me declare a useless default clause.")

What that leaves is that the legacy (constants only) statement switches 
are not required to be total, and therefore do not receive the 
compile-time checking for totality (which would have made this puzzler 
somewhat more obvious) that all other switches receive.

Over time, more switches will be expressions and fewer will be 
statements.  Over time, more switches will use patterns, and fewer will 
use constants.  So in the fullness of time, the case of 
legacy&&statement switches will become more of a corner case.

As has been mentioned (not just on this thread), we have the option to 
issue warnings for the cases where we cannot change the behavior of 
constructs with existing meaning.  We may do this, in the future, but if 
Bearded Dimitris is not complaining now, if we move too aggressively, he 
surely will.  So we're taking it one step at a time.

> Little I know, I was "surprised" to find out that I could not 
> initialize the final variable in a seemingly exhaustive switch.

Yes, you got surprised by two things:

  - Seemingly exhaustive switches over enums and sealed types, that are 
not really exhaustive because of the possibility for novel 
constants/subtypes appearing at runtime;
  - That we could give expression switches updated semantics, as well as 
any switch over a new type, or with patterns, but were kind of stuck 
with the legacy switches (restricted types, constants only, statements).

We have done a lot to minimize the pain here, because we don't want 
users to have to worry about "what if someone adds a new constant to my 
Boolean enum."  When we did expression switches, which must be total, if 
the switch is over an enum (and later, a sealed type), we require 
totality and, if there is no default, insert a throwing default so the 
user doesn't have to handle this weird case.  We later did the same with 
statement switches that are not legacy constant switches.  But there's a 
small slice we couldn't come back for, and at this current point in 
history, it represents a greater portion of switches (because until 
recently, it was the only kind that was allowed) than it will in the 
future.

> } else { //Hidden
>     throw
> }

We already do this where we can, which is:
  - expression switches over enums
  - any switch over sealed types

> So why can't switch statements enjoy the totality of expressions too?

Yes, we spent a lot of time grappling with this, and concluded that the 
best move that we could make at the current time is to do this for any 
switch over sealed types, and any expression switch, but that still 
leaves enum statement switches in the cold.

> enum Parity {ODD, EVEN}
> final int a;
>  total switch (p) {
>     case Parity.ODD -> a = 1;
>     case Parity.EVEN -> a = 0;
> }
> int b = a + 1; //Compiles

We discussed this idea somewhat extensively, but it was pretty clear 
that it was pushing on the short end of the lever; it would not have 
helped your initial surprised, because you didn't know to insert total 
in this case.  So it only helps you lock the barn door after the horse 
has escaped.

A much better way to do this is to just use a switch expression! This is 
not only better because of totality checking (that's just a bonus), it's 
better because expressions are better than statements. The statement 
switch relies on mutation, for which we need DA/DU analysis to try to 
make safer; the expression version is obviously correct:

     int a = switch (p) {
         case ODD -> 1;
         case EVEN -> 0;
         // implicit hidden throwing default
    }

Refactor your total legacy statement switches to expression switches, 
and the puzzler vanishes, and your code gets better.

> That shouldn't stop us
> from adding total switch statements.

And it hasn't.  What it stops us from is changing the meaning of 
existing statement switches.  (Which is really restricted to statement 
switches over enums.)




More information about the amber-dev mailing list