The issue is not restricted to variables declared in loop headers, so the proposed loop change for Go might only be the start.
AFAICT it doesn't simplify javac much, if at all. It still needs to synthesize closure objects with fields to store the closed-over values. It's just that those fields can be final.
I think Java did this to avoid programmer confusion. I think it was the right choice.
void foo() {
int a = 1;
Runnable r = () -> System.out.println(a);
r.run(); // prints 1
}
Can get turned into something like this: class r_closure implements Runnable {
final int a;
r_closure(int a) { this.a = a; }
@Override
public void run() { System.out.println(a); }
}
void foo() {
int a = 1;
Runnable r = new r_closure(a);
r.run(); // prints 1
}
The local a is a perfectly normal local, and the field a is a perfectly normal field.What would have to happen if the variable was mutable? For example, if you wanted to write this:
void foo() {
int a = 1;
Runnable r = () -> System.out.println(a);
a = 2;
r.run(); // prints 2
}
You have to transform it to something like this: class r_closure implements Runnable {
int a;
r_closure(int a) { this.a = a; }
@Override
public void run() { System.out.println(a); }
}
void foo() {
int _a = 1;
r_closure r = new r_closure(_a);
r.a = 2;
r.run();
}
Where there is no local, and where the method looks like it's accessing a local, it's actually reaching into the closure and mutating its field!Now think about doing this if you've captured a variable in two closures, or a variable number of closures in a list. The wheels come off this approach.
Instead, you would have to promote the shared mutable variable to its own object, like this:
class int_box {
int i;
int_box(int i) { this.i = i; }
}
class q_closure implements Runnable {
final int_box a;
q_closure(int_box a) { this.a = a; }
@Override
public void run() { System.out.println("q = " + a.i); }
}
class r_closure implements Runnable {
final int_box a;
r_closure(int_box a) { this.a = a; }
@Override
public void run() { System.out.println("r = " + a.i); }
}
void foo() {
int_box a = new int_box(1);
Runnable q = new q_closure(a);
Runnable r = new r_closure(a);
a.i = 2;
q.run(); // prints 2
r.run(); // prints 2
}
Now you've taken a simple local variable which just need to be copied, and turned into its own thing on the heap!Technically it’s not but practically it’s by far the most common way for this to unexpectedly arise.
The other cases like closing over a variable and then modifying it before the closure is invoked are a lot less common to hit unexpectedly, and a lot harder to fix nicely (short of Java’s big hammer).
the fix was usually adding x := x
before go func() { do something with x }
`go` evaluates everything but the final function call in the context of the caller. So
go func (x int) { … } (x)
Will do the same, and is easier to extract to a named function. go func (x int) { do_work(x) } (x)
seems like a very roundabout way to say go do_work(x)
The "x := x" solution too suffers from this problem but slightly less: both idioms look like they are no-ops (while they are actually not) but at least "x := x" is weird enough to look like it was a deliberate choice, not some vestige from refactoring.Your first version is in fact a worse way to write the second one.
What's even the point of capturing the variable itself? To allow for writing inline callbacks that could sneakily mutate loop-local variables?
That’s how you’d do it using a [=] lambda in c++ or a move closure in rust.
Then again, Python has the same syntax for assignment and initialization.