What are the correct semantics of a closure over a

2019-03-01 08:03发布

问题:

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?"

回答1:

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.



回答2:

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.



回答3:

"what is the behavior of closing over a looped variable in lambda calculus?"

There are no loop variables in lambda calculus.



回答4:

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.