It became an issue as lambdas and other lambda-type constructs (which implicitly keep a reference on the loop variable) became more common, and a bunch of languages got caught in it. Later languages switched to the “inner scoping” mechanism to avoid it.
Go did not follow the switch because Go.
nums := []int{1, 2, 3}
for _, num := range nums {
fmt.Printf("%p\n", &num)
}
This will print the same address three times. If you add "num := num" as the first line in the loop, it will print three different addresses. The proposal is to make this the default behaviour.Yes it is.
> The variable is scoped to the loop.
Maybe “loop body” would make the comment clearer?
> The issue is that even if you take the address of the variable, each iteration of the loop will have the same variable with the same address.
Hence the variable not being scoped to the loop (body), one variable is shared between all iterations of the loop.
I think "loop iteration" might.
for x in range(3):
foo(x)
print(x) # prints 2
or the pre-C99 style: int i;
for (i = 0; i < 3; ++i)
foo(i)
printf("%d", i); // prints 3
This is not the case in Go. I don't think talking about variable scopes accurately describes the issue (because there's nothing special about loop scopes here: the same "escape" can happen from any scope), and changing "loop" to "loop body" doesn't improve this. The term "loop iteration" at least identifies the dependency between different iterations of the loop as the issue.I don't understand what the thought experiment about non-lexical scopes has to do with this.
for _, elem := range elems {
elem := elem
... &elem ...
}
Nothing beyond regular lexical scoping and Go's ordinary assignment semantics are necessary to see how this works. The second 'elem' has a narrower scope than the first (it is limited to the loop body). Abusing Go syntax, you can think of the current semantics as follows: {
var elem Elem
for _, elem = range elems {
... &elem ...
}
}
Here 'elem' scopes outside the loop body, and so is reassigned on every iteration of the loop (and &elem evaluates to the same address on every iteration).>the thought experiment about non-lexical scopes
It's not just a thought experiment. There are languages with dynamically scoped variables (e.g. global vars in Common Lisp).
I guess taking the address "promoted" the shared variable to enable it to survive past the function?
1. Java is also supposed to do that, and clearly way less successful
2. Escape log is part of the standard baseline tools (it’s just a flag), so it’s not considered anything hidden or arcane
3. Go tends to log escapes, meaning it’s baseline is to stack allocate
So it’s a lot more than, say, common subexpression optimisation.
The default is heap, and only when the compiler can be sure it's safe, things go on the stack.
But the problem should have been known when the language was designed. In CommonLisp, i.e., quite a mighty but old language, it is the same: the capture is on a loop variable that is destructively updated:
(setf refs (loop for i in '(1 2 3) collect #'(lambda () i)))
-> (#<function) #<function> #<function>)
(mapcar #'(lambda (f) (funcall f)) refs)
-> (3 3 3)
The same done with a fresh function parameter works. And since this is the usual style in Lisp, I suppose the problem will not be that obvious like in Go: (setf refs (mapcar #'(lambda (i) #'(lambda () i)) '(1 2 3)))
-> (#<function> #<function> #<function>)
(mapcar #'(lambda (f) (funcall f)) refs)
-> (1 2 3) >>> def foo():
... return [lambda: x for x in range(10)]
...
>>> [x() for x in foo()]
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9]A common sentiment when Go was unveiled was that its designers had ignored the 35-odd years of language research and experience since C.
>>> [f() for f in (lambda: x for x in range(10))]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
List comprehensions have been around for longer, and so behave the way they do for backwards compatibility reasons.I suppose it's the natural way to do it when implementing a language and not thinking about it too much. It makes a for-loop equivalent to a simple while loop with the loop vvariable initialized outside of the loop.
TL;DR is that both "for" and "foreach" scoping fixes would be breaking changes, but "foreach" was easier to justify because it was already a C#-specific construct syntactically, unlike "for" which uses the same exact syntax as C, Java etc, and they were very sensitive to backwards compatibility at the time (esp. since the tooling didn't have the ability to target various language versions within the same project easily). At the same time, "foreach" represented the vast majority of breakage when they looked at existing code, perhaps because the scoping in classic "for" is more obvious due to the fact that variable mutation is explicit there.
initVariable()
while checkVariable():
doSomethong…
updateVariable()
So the variable is shared by all iterations at first place. And that isn't a issue until we have lambda or something similar that can capture reference of loop variable.Later languages found it is problematic when use with reference capturing features and changed to something else (one variable per iteration)
Complexity increases rapidly when you combine constructs in a programming language. You get some feature interactions which are hard to get right, and also to predict.