Guard variable and being effectively final

Dimitris Paltatzidis dcrystalmails at gmail.com
Sat May 21 19:08:50 UTC 2022


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