[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