2011-10-10
Arranging scopes for for
loops
Suppose that you have a language where for loop index variables are local to the for loop; they aren't visible outside it. What I'll call the 'for loop closure problem' is that in a straightforward implementation of their lifetime, the following pseudo-code doesn't work the way people think it will:
nums := []; for (var i = 0; i < 100; i++) nums.append(func(x) {i+x});
In straightforward scope and variable lifetime semantics, every
lambda returns the same result because each lambda captured
the same i
(and after the loop finishes, the value of i
is
100). When I was discussing what Python closures close over, I mentioned that you could fix this with
careful use of how scopes are defined.
First, let's rewrite this for loop to show the scope explicitly:
{var i; for (i = 0; i < 100; i++) nums.append(func(x) {i+x}); }
If we want each iteration of the loop (and thus each lambda) to get
a different version of i
, we need to explicitly create a scope for
each of them. However, we need all of the following to be true:
- the for loop condition has a single sustained scope, in which its
version of
i
is the same over all iterations of the loop. This creates an outer version ofi
. - the for loop body has a per-iteration scope which contains an inner
version of
i
. - the inner version of
i
is initialized from the outer version ofi
.
Of these the only tricky requirement is the third one.
You could fix this with specific rules for scopes in for
loops, but as
it happens we don't have to bother with this; the language already has
a way of expressing this outcome within the existing scoping rules. We
just say that the loop body acts as if it was an anonymous function
(well, closure) that is invoked on each loop iteration. This effectively
rewrites the loop as:
{var i; for (i = 0; i < 100; i++) func(i) { nums.append(func(x) {i+x}); }(i); }
Anonymous functions already create new scopes when invoked and function calls are the general method for passing variables into a scope, so we're done.
(Various sorts of optimizations can and generally are applied in languages that define for loops this way, because you really don't want to be creating a call frame for every loop iteration.)
I believe that doing this sort of conceptual rewriting is especially popular in languages (like many versions of Lisp) that consider lambdas and closures to be fundamental building blocks of the language. In these languages, rewriting loops as invocation of anonymous functions makes a lot of sense even if you don't care about the scoping rules. The extreme version is to not even have loop iteration as such in the language's core; instead you transform loops into recursive function calls. (Full tail call optimization is required for obvious reasons.)