Consider the following lua code:
f = {}
for i = 1, 10 do
f[i] = function()
print(i .. " ")
end
end
for k = 1, 10 do
f[k]()
end
This prints the numbers from 1 to 10. In this case, i
is closed over the value for each iteration of the outer loop. This is how I had always understood closures, and I was very happy...
...until I was porting some lua code into c#, and I tried to do the same thing:
var f = new Action[10];
for (int i = 0; i < 10; i++)
{
f[i] = (new Action(delegate()
{
Console.Write(i + " ");
}));
}
for (int k = 0; k < 10; k++)
{
f[k]();
}
And now I get the number 10 printed 10 times (let's forget that lua arrays are 1-based). It actually happens that in this case, the closure works over the variable, not its value, which makes a lot of sense, since I'm only calling the functions once the first loop is over.
JavaScript seems to have the same semantics (close over the variable):
var f = []
for (var i = 0; i < 10; i++)
{
f[i] = function()
{
document.write(i + ' ');
};
}
for (var k = 0; k < 10; k++)
{
f[k]();
}
Actually, both behaviors make a lot of sense, but are of course incompatible.
If there is a "correct" way to do this, then either lua, or c# and JavaScript are wrong (I haven't tried with other languages yet). So my question is: "what are the "correct" semantics of closing a variable inside a loop?"
edit: I'm not asking how to "fix" this. I know I can add a local variable inside the loop and close over that one to get the lua behavior in c#/JavaScript. I want to know what is the theoretically correct meaning of closing over a looped variable is, and bonus points for a short list of which languages implement closures in each way.
edit: To rephrase my question: "what is the behavior of closing over a looped variable in lambda calculus?"
The Lua manual explains exactly why this works. It describes the index for-loop in terms of a while loop as this:
for v = e1, e2, e3 do block end
--Is equivalent to:
do
local var, limit, step = tonumber(e1), tonumber(e2), tonumber(e3)
if not (var and limit and step) then error() end
while (step > 0 and var <= limit) or (step <= 0 and var >= limit) do
local v = var
block
var = var + step
end
end
Notice how the loop variable v
is declared inside the scope of the while
loop. This is done specifically to allow exactly what you're doing.
There is no "correct" way. There are different ways. In C#, you would fix it by making a variable scoped to the loop:
for (int i = 0; i < 10; i++)
{
int j = i;
f[i] = (new Action(delegate()
{
Console.Write(j + " ");
}));
}
In JavaScript, you might add a scope by making and calling an anonymous function:
for (var i = 0; i < 10; i++) {
(function(i) {
f[i] = function() {
document.write(i + ' ');
};
})(i);
}
Iteration variables in C# don't have loop scope. JavaScript doesn't have block scope, just function scope. They're just different languages and they do things differently.
"what is the behavior of closing over a looped variable in lambda calculus?"
There are no loop variables in lambda calculus.
Closing over a loop variable is like closing over any other variable. The problem is with language-specific looping constructs and whether they translate into code that puts the loop variable inside or outside the loop.
For instance, if you use a while
loop in C#, Lua or JavaScript, the result in all three languages is the same (10). Ditto for a for(;;)
loop in JavaScript or C# (not available in Lua).
However, if you use a for (i in x)
loop in JavaScript, you'll find that each closure gets a new copy of i
(output: 0 1 2 3 ...
). Ditto for for i=x,y
in Lua and foreach
in C#. Again, that has to do with how those languages construct those loops and how they expose the value of the loop variable to the body of the loop, not a difference in closure semantics.
In fact, in the case of C#'s foreach
, this behavior changed from 4.5 to 5. This construct:
foreach (var x in l) { <loop body> }
Used to translate into (pseudocode):
E e = l.GetEnumerator()
V v
while (e.MoveNext()) {
v = e.Current
<loop body>
}
In C# 5, this was changed to:
E e = l.GetEnumerator()
while (e.MoveNext()) {
V v = e.Current
<loop body>
}
This was a breaking change, done to better meet programmer expectations when closing over the loop variable. The closure semantics didn't change; the position of the loop variable did.