Guard variable and being effectively final
Remi Forax
forax at univ-mlv.fr
Sun May 22 11:10:04 UTC 2022
To take your example,
this example of code is fine
final int a;
if (Math.random() < 0.5) {a = 1;}
while this one
final int a;
if (Math.random() < 0.5) {a = 1;}
if (Math.random() < 0.5) {a = 1;}
is not.
Like in
Object o = ...
int i = switch (o) {
case String s && (b = true) -> 1;
default -> 0;
};
the local variable is assigned once that why i think this code should compile.
Also beware of equivalences, "final" in static final and "final" on a local variable are two different beasts.
and yes, it's an example to explain why Map::get as a pattern is useful.
regards,
Rémi
PS: in your code with an Optional, you should use orElseGet() and there is a bug in my code, it should be
var value2 = stack.pop();
var value1 = stack.pop();
otherwise, it does not work if the operator is not commutative.
----- Original Message -----
> From: "Dimitris Paltatzidis" <dcrystalmails at gmail.com>
> To: "amber-dev" <amber-dev at openjdk.java.net>
> Sent: Saturday, May 21, 2022 9:08:50 PM
> Subject: Guard variable and being effectively final
> Bug or feature?
> Let's capture the essence with a simpler, yet silly example:
>
> final boolean b;
> Object o = ...
> int i = switch (o) {
> case String s && (b = true) -> 1;
> default -> 0;
> };
>
> we still get - java: local variables referenced from a guard must be final
> or effectively final .
> To better understand (hopefully) what's going on, let's take a look at
> static final field initialization:
>
> class C {
> static final int a;
> static {C.a = 1;}
> }
>
> It won't compile - java: cannot assign a value to final variable a .
> The problem is in C.a = 1; it has to be just a = 1; Access to field a is
> "overloaded" with 2 stories:
> 1. C.a = 1 is in defence: "I don't care, field a is final, that's illegal."
> 2. a = 1 is more open: "If a is not initialized, I got you, otherwise be
> careful"
>
> a = 1 is reduced to C.a = 1 after the first assignment.
> Bringing this madness to our case, the compiler just might be dealing with
> local variables in guards as the above case 1. (C.a = 1).
> We will never be able to initialize, because we have read-only access to
> our hands.
>
> What you are arguing for is, why the compiler plays with the rules of case
> 1. and not 2. which eventually renders down to, is it actually a bug?
> It could be a half-bug, that is, playing it too safe, by blocking write
> access. Sometimes, it's smart thought, the below compiles (if not IDE
> magic):
>
> final int a;
> do {a = 1;} while (false);
>
> My understanding is that, final variable initialization in guards is
> forbidden to defend against successful assignments, but failed conditions:
>
> final boolean b;
> Object o = ...
> int i = switch (o) {
> case CharSequence s && (b = Math.random() < 0.5) -> 1;
> case String s && (b = Math.random() < 0.5) -> 2; //No dominance
> problem, cause guards
> default -> 0;
> };
>
> If o is not a String, the above should not have any problem, otherwise
> re-assignment is inevitable. We never know beforehand, only on runtime,
> but that's too late for a compile time error, isn't it. One might say,
> because of the second case, block compilation, which is fair, considering
> the
> below won't compile either:
>
> final int a;
> if (Math.random() < 0.5) {a = 1;}
> if (Math.random() < 0.5) {a = 2;}
>
> Again, your argument is on why single guarded-case switch expressions are
> restrictive as well. It might all have to do with the default:
>
> final boolean b;
> Object o = ...
> int i = switch (o) {
> case String s && (b = Math.random() < 0.5) -> 1;
> default -> {b = false; yield 0;}
> };
>
> Again, we don't know if the cat is alive or dead.
> Of course, one could argue that in those situations, just raise a compile
> error at the default-case site.
>
> Adding more to the confusion, Yes; my biological compiler parses your code
> just fine. It just might all boil down to economic activity:
> "Would we get enough returns for our more out-of-jail compiler-cards
> investments?"
>
> static int eval(List<String> expr) {
>> var stack = new ArrayDeque<Integer>();
>> for(var token: expr) {
>> final IntBinaryOperator op;
>> stack.push(switch (token) {
>> case String __ && (op = OPS.get(token)) != null -> {
>> var value1 = stack.pop();
>> var value2 = stack.pop();
>> yield op.applyAsInt(value1, value2);
>> }
>> default -> Integer.parseInt(token);
>> });
>> }
>> return stack.pop();
>> }
>>
>> static int eval(List<String> expr) {
> var stack = new ArrayDeque<Integer>();
> for(var token: expr) {
> stack.push(Optional.ofNullable(OPS.get(token))
> .map(op ->
> op.applyAsInt(stack.pop(), stack.pop()))
> .orElse(Integer.parseInt(token)));
> }
> return stack.pop();
> }
>
> Of course I just defeated your purpose, didn't I, as there is an
> optimization twist in your code: "Don't even bother diving into the map if
> it ain't a String".
> Turns out, token can only be a String, so why even bother with pattern
> matching switch (of course I still get what you are trying to get across).
> It would be a richer story if Map::get was a pattern.
More information about the amber-dev
mailing list