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