Final variable initialization problem with exhaustive switch

Remi Forax forax at univ-mlv.fr
Tue Nov 30 16:01:34 UTC 2021


----- Original Message -----
> From: "Brian Goetz" <brian.goetz at oracle.com>
> To: "Dimitris Paltatzidis" <dcrystalmails at gmail.com>
> Cc: "John Rose" <john.r.rose at oracle.com>, "amber-dev" <amber-dev at openjdk.java.net>
> Sent: Mardi 30 Novembre 2021 16:39:05
> Subject: Re: Final variable initialization problem with exhaustive switch

> This is a sensible-seeming and well-intentioned argument, but it is
> unfortunately not correct.
> 
> The problem is one of separate compilation.  At compile time, there are only two
> enum constants, EVEN and ODD, but at run time, there may be more (someone could
> edit Parity to add INDETERMINATE, and not recompile the client code.)  If such
> a novel constant shows up at runtime, the program semantics are subverted.
> 
> Combine this with the fact that historically, switch statements are inherently
> partial; the switch you wrote currently *has a meaning*, which is if a novel
> value shows up, do neither of the actions.  This might be a “dumb” meaning, but
> it is the meaning existing switches have, and we can’t arbitrarily change it.
> 
> What you are arguing is that we made bad decisions in 1995 by allowing partial
> switches at all; while I might agree with that, in hindsight, it was “obvious”
> in 1995 that partiality was an inherent aspect of switch statements (because,
> C.)  When we added switch *expressions*, totality was a forced move (and a good
> one); when we extended switches to support patterns, we made the bold move
> (which will annoy some) to require that non-legacy switch STATEMENTS also be
> total.  But we can’t rewrite history.
> 
> We toyed with the idea of having a modifier on switch (e.g. “total-switch”)
> which would engage the totality checking, but the return here seemed weak; it
> would only work if people changed their code.   We could over a decade possibly
> nudge pattern switches towards totality, with a gradually increasing set of
> warnings which eventually became errors, and we might do that, but let’s not
> kid ourselves that this is a very long road.

It's a long road but any stations along that road is a better place than the one we are currently.

Dimitris send me the code privately before and ask me if it was a bug or not.
I answer that it was a bug, I was wrong. I'm pretty sure Dimitris and me are not the only ones to fall into that trap.

Adding a warning requiring a default might be a good step forward, even if we never do more than that.

> 
> We are largely in a no-win situation; we can either have newer constructs
> inherit the mistakes of the past, or live with some seam of “old” switches
> working slightly differently.  We can try to move that seam around, but its
> hard to have it all.  Your claim of “inconsistency” illustrates that fact, but
> if you had consistency, you wouldn’t like it, because we’d have to be
> consistent with the bad old behavior.
> 
> Of course, whether this is “too” safe is a matter of opinion.  It is a popular
> viewpoint to say “but its wrong, just go ahead and break other people’s code,
> its good for them.”  But people won’t thank you for educating them; not doing
> so (or doing so only very, very carefully) is how Java stays popular.  So while
> I might call it unfortunate, it’s not “too” safe; its just safe.

Fortunately, a warning does not break the code.

Rémi


> 
>> On Nov 30, 2021, at 7:49 AM, Dimitris Paltatzidis <dcrystalmails at gmail.com>
>> wrote:
>> 
>>> 
>>> seems like
>>> it should be a bug, right?  But it’s not.
>>> 
>> 
>> Agree, it is just playing it too safe.
>> 
>> We can translate the above switch statement into an if, without losing the
>> semantics:
>> 
>> if (p == Parity.ODD) {
>>    a = 1;
>> } else if (p == Parity.EVEN) {
>>    a = 0;
>> }
>> int b = a + 1; //Compile time error: variable a might not have been
>> initialized
>> 
>> The above if is exhaustive, yet no one expects last statement to compile,
>> and that is totally fine.
>> So, reducing the switch to the above if, makes it clear why the compiler
>> can't guarantee about the switch too.
>> But, maybe it should. You see, to let the above if compile is an overkill,
>> but with the switch, we have the notion
>> of its context, and especially with enums and sealed types, we can take it
>> a step further and make assumptions
>> about its exhaustiveness.
>> 
>> The switch expression guarantees initialization of final variables, based
>> on whether it compiles or not, which by
>> itself depends on its exhaustiveness.
>> The switch statement does not have to be exhaustive to compile and
>> therefore there isn't here that compilation
>> barrier that guarantees the initialization. But, that doesn't have to be
>> the case. Our switch statement in question
>> is *Effectively *exhaustive. So, the compiler doesn't need to prove that a
>> switch statement is exhaustive, it is
>> sufficient to just prove that it is effectively exhaustive. It is similar
>> to the effectively final variables we have with
>> lambdas.
>> 
>> Of Course, at the end of the day, it's not about exhaustiveness, it's about
>> having a branch that is guaranteed
>> to perform the initialization. That is what we are effectively trying to
>> prove.
>> 
>> Lately, the switch has been greatly enhanced, especially in the
>> exhaustiveness department. Getting the
>> switch statement on track in that aspect, seems more consistent.
>> 
>> Στις Τρί, 30 Νοε 2021 στις 9:20 π.μ., ο/η John Rose <john.r.rose at oracle.com>
>> έγραψε:
>> 
>>> On Nov 29, 2021, at 12:04 PM, Dimitris Paltatzidis <
>>> dcrystalmails at gmail.com> wrote:
>>> 
>>> 
>>> How can the compiler prove that the final variable will be initialized only
>>> in the second case and not in the first too?
>>> 
>>> 
>>> That differing treatment of definite assignment seems like
>>> it should be a bug, right?  But it’s not.  In fact, your switch
>>> expression can (under certain circumstances) execute
>>> correctly without executing either of its case branches.
>>> 
>>> Switch expressions are inherently exhaustive across their cases.
>>> Switch statements are not.  This is a key difference.
>>> 
>>> You can see this if you take your code and comment out
>>> one of the cases, say for Parity.EVEN.  The switch expression
>>> errors out at compile time, but the switch statement is a
>>> happy camper.
>>> 
>>> Or, if someone comes along and adds a third member to Parity
>>> (say, NEITHER) then your switch expression will either fail to
>>> compile statically or (if not recompiled) will fail to terminate
>>> normally at runtime (throwing something).
>>> 
>>> Meanwhile, the corresponding switch statement will happily
>>> recompile, and (whether recompiled or not) will fall through
>>> when Parity.NEITHER is presented.  In that case, the variable
>>> in question will not have been initialized.
>>> 
>>> This is a problem with legacy switch statements.  I’m not sure
>>> what the solution will be, although I know various alternatives
>>> have been discussed at some point.  The problem is that the
>>> language cannot read the user’s mind, when it finds that
>>> (a) an enum is the subject of a switch statement but (b) not
>>> all members of the enum are mentioned in the cases.  Is it
>>> an error in a switch that was intended to be exhaustive?
>>> Or is it just that the user (that lazy user!) didn’t want to
>>> mention cases which needed no-op actions?
>>> 
>>> — John


More information about the amber-dev mailing list