Tennent's correspondence principle and loop-local variable capture

# Vladimir Sedach (14 years ago)

Hello,

I was recently pointed to the block scoping and TCP discussions by Peter Michaux, and have a question as to how this will affect the capture of loop-local variables by closures created in loops.

Some programming languages (Common Lisp in particular) assign a new binding to loop-local variables on every iteration. JS doesn't do this, but there is a way to introduce a new lexical environment on every iteration with the WITH statement, an idea that I first encountered in the Scheme2JS compiler (www-sop.inria.fr/indes/scheme2js). Parenscript uses this trick for translating Common Lisp to JS (a good discussion of how this shows up in loops is here: lists.common-lisp.net/pipermail/parenscript-devel/2010-December/000914.html).

However the WITH statement is deprecated. How can per-iteration loop-local variable capture be accomplished with the current proposal, and what effect does the Block Lambda Revival proposal have on this issue?

Thank you, Vladimir

# Brendan Eich (14 years ago)

On Jun 30, 2011, at 3:33 PM, Vladimir Sedach wrote:

Hello,

I was recently pointed to the block scoping and TCP discussions by Peter Michaux, and have a question as to how this will affect the capture of loop-local variables by closures created in loops.

A well-known sore point.

The good news is that for-in and for-of loops in ES.next, when using let (or if implicitly using let, in comprehensions, generator expressions, and possibly even the for-of loop statmeent) not var, will result in a fresh binding per iteration:

harmony:iterators

Some programming languages (Common Lisp in particular) assign a new binding to loop-local variables on every iteration. JS doesn't do this, but there is a way to introduce a new lexical environment on every iteration with the WITH statement, an idea that I first encountered in the Scheme2JS compiler (www-sop.inria.fr/indes/scheme2js). Parenscript uses this trick for translating Common Lisp to JS (a good discussion of how this shows up in loops is here: lists.common-lisp.net/pipermail/parenscript-devel/2010-December/000914.html).

However the WITH statement is deprecated. How can per-iteration loop-local variable capture be accomplished with the current proposal, and what effect does the Block Lambda Revival proposal have on this issue?

See above. 'with' is not lexical in any sense, and it has been removed from ES5 strict mode, so it is gone from Harmony / ES.next.

'let', on the other hand, is the new var, and we have the opportunity to make a fresh binding per iteration when for-in/-of is used with 'let'. This is the way to go.

The old C-style for(;;) loop is a hard case. It really does want a single shared-mutable loop control variable. Making 'let' instead of var in for (let i = 0; i < N; i++) ... bind afresh on each iteration and communicate the ++ update across the loop edge is theoretically doable, but it's wrong. There's really only one binding there.

So for-in/-of will benefit with 'let'. Is that enough? If not, you can always nest an explicit block inside the loop and capture its 'let' bindings.

# Mark S. Miller (14 years ago)

On Thu, Jun 30, 2011 at 4:07 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Jun 30, 2011, at 3:33 PM, Vladimir Sedach wrote:

Hello,

I was recently pointed to the block scoping and TCP discussions by Peter Michaux, and have a question as to how this will affect the capture of loop-local variables by closures created in loops.

A well-known sore point.

The good news is that for-in and for-of loops in ES.next, when using let (or if implicitly using let, in comprehensions, generator expressions, and possibly even the for-of loop statmeent) not var, will result in a fresh binding per iteration:

harmony:iterators

Some programming languages (Common Lisp in particular) assign a new binding to loop-local variables on every iteration. JS doesn't do this, but there is a way to introduce a new lexical environment on every iteration with the WITH statement, an idea that I first encountered in the Scheme2JS compiler (www-sop.inria.fr/indes/scheme2js). Parenscript uses this trick for translating Common Lisp to JS (a good discussion of how this shows up in loops is here:

lists.common-lisp.net/pipermail/parenscript-devel/2010-December/000914.html ).

However the WITH statement is deprecated. How can per-iteration loop-local variable capture be accomplished with the current proposal, and what effect does the Block Lambda Revival proposal have on this issue?

See above. 'with' is not lexical in any sense, and it has been removed from ES5 strict mode, so it is gone from Harmony / ES.next.

'let', on the other hand, is the new var, and we have the opportunity to make a fresh binding per iteration when for-in/-of is used with 'let'. This is the way to go.

The old C-style for(;;) loop is a hard case. It really does want a single shared-mutable loop control variable. Making 'let' instead of var in for (let i = 0; i < N; i++) ... bind afresh on each iteration and communicate the ++ update across the loop edge is theoretically doable, but it's wrong.

Why is it wrong? I showed a de-sugaring of for(;;) into lambda that creates a separate binding per iteration. (I can try to find it in the es-discuss archive if it would be helpful.)

# Jon Zeppieri (14 years ago)

On Thu, Jun 30, 2011 at 7:27 PM, Mark S. Miller <erights at google.com> wrote:

On Thu, Jun 30, 2011 at 4:07 PM, Brendan Eich <brendan at mozilla.com> wrote:

The old C-style for(;;) loop is a hard case. It really does want a single shared-mutable loop control variable. Making 'let' instead of var in for (let i = 0; i < N; i++) ... bind afresh on each iteration and communicate the ++ update across the loop edge is theoretically doable, but it's wrong.

Why is it wrong? I showed a de-sugaring of for(;;) into lambda that creates a separate binding per iteration. (I can try to find it in the es-discuss archive if it would be helpful.)

Because when I write i++ I expect mutation.

# Brendan Eich (14 years ago)

On Jun 30, 2011, at 4:27 PM, Mark S. Miller wrote:

The old C-style for(;;) loop is a hard case. It really does want a single shared-mutable loop control variable. Making 'let' instead of var in for (let i = 0; i < N; i++) ... bind afresh on each iteration and communicate the ++ update across the loop edge is theoretically doable, but it's wrong.

Why is it wrong? I showed a de-sugaring of for(;;) into lambda that creates a separate binding per iteration. (I can try to find it in the es-discuss archive if it would be helpful.)

Here it is:

esdiscuss/2008-October/007819

The whole thread is at

esdiscuss/2008-October/thread.html#7844

Please see Jon Zeppieri's reply in particular, quote:

I think the real problem here is that the <updateExpr>, as written by the user, is (or, rather, usually

is) an assignment, and you should give the user what the user asked for.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ...

-Jon

The for(;;) loop is unstructured, the update part after ; really is any mutating expression you want (or a pure no-op, even). Specializing only let binding updates of certain forms (++, +=, -=, *=, what else? How about calls to functions that capture the loop variable and mutate it?) is too subtle and special. That's why I call it "wrong".

Let's not get hung up on the for(;;) loop. It is an imperative construct.

I think a more important extension to consider, where we have new syntax on which to cleanly hang fresh bindings, is something from Algol and C++: allowing 'let' to start the condition or discriminant in if, while, and switch heads:

if (let tmp = expensive_condition()) { // tmp in scope here and here only. }

while (let j = frob(i)) { // j in scope and fresh each iteration here. i = update(i); }

etc.

# Mark S. Miller (14 years ago)

On Thu, Jun 30, 2011 at 4:39 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Jun 30, 2011, at 4:27 PM, Mark S. Miller wrote:

The old C-style for(;;) loop is a hard case. It really does want a single

shared-mutable loop control variable. Making 'let' instead of var in for (let i = 0; i < N; i++) ... bind afresh on each iteration and communicate the ++ update across the loop edge is theoretically doable, but it's wrong.

Why is it wrong? I showed a de-sugaring of for(;;) into lambda that creates a separate binding per iteration. (I can try to find it in the es-discuss archive if it would be helpful.)

Here it is:

esdiscuss/2008-October/007819

That's the one, thanks.

The whole thread is at

esdiscuss/2008-October/thread.html#7844

Please see Jon Zeppieri's reply in particular, quote:

I think the real problem here is that the <updateExpr>, as written by the user, is (or, rather, usually is) an assignment, and you should give the user what the user asked for.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ...

-Jon


This is a valid criticism.

The for(;;) loop is unstructured, the update part after ; really is any mutating expression you want (or a pure no-op, even). Specializing only let binding updates of certain forms (++, +=, -=, *=, what else? How about calls to functions that capture the loop variable and mutate it?) is too subtle and special. That's why I call it "wrong".

This is not. My desugaring does not descend into, special case, or transform the updateExpr. Any Expression may appear here. In fact, as far as my desugaring is concerned, any Statement could appear here.

Let's not get hung up on the for(;;) loop. It is an imperative construct.

I don't feel strongly about this -- it's just an "it would be nice if we could fix this." If not, I won't shed any tears.

I think a more important extension to consider, where we have new syntax on which to cleanly hang fresh bindings, is something from Algol and C++: allowing 'let' to start the condition or discriminant in if, while, and switch heads:

if (let tmp = expensive_condition()) { // tmp in scope here and here only. }

while (let j = frob(i)) { // j in scope and fresh each iteration here. i = update(i); }

Interesting.

# Brendan Eich (14 years ago)

On Jun 30, 2011, at 4:47 PM, Mark S. Miller wrote:

The whole thread is at

esdiscuss/2008-October/thread.html#7844

Please see Jon Zeppieri's reply in particular, quote:

I think the real problem here is that the <updateExpr>, as written by the user, is (or, rather, usually is) an assignment, and you should give the user what the user asked for.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ... -Jon

This is a valid criticism.

It is, and you're right, it's not the same as the one I tried to get away with below:

The for(;;) loop is unstructured, the update part after ; really is any mutating expression you want (or a pure no-op, even). Specializing only let binding updates of certain forms (++, +=, -=, *=, what else? How about calls to functions that capture the loop variable and mutate it?) is too subtle and special. That's why I call it "wrong".

This is not. My desugaring does not descend into, special case, or transform the updateExpr. Any Expression may appear here. In fact, as far as my desugaring is concerned, any Statement could appear here.

But Jon's post, a bit earlier, pointed out how your desugaring did not handle all cases. This led me to think you weren't trying to handle them all. Sorry about that!

Let's not get hung up on the for(;;) loop. It is an imperative construct.

I don't feel strongly about this -- it's just an "it would be nice if we could fix this." If not, I won't shed any tears.

No tears, no fears. Jon's point about pulling the let out before the loop changing things seems solid. I won't try to generalize it much, but here's an attempt:

So far, prior to 'let', JS's C-style

for (init; cond; update { body }

has been what it is in C: sugar for

init while (cond) { body update }

This implies not coupling the parts in new ways, such as a fresh-per-iteration binding when init is "let i = 0" that is initialized only the first time by that " = 0" initialiser, and somewhat magically initialized subsequently from the previous iteration's post-update value of the binding.

See what I mean?

# Mark S. Miller (14 years ago)

On Thu, Jun 30, 2011 at 4:59 PM, Brendan Eich <brendan at mozilla.com> wrote:

On Jun 30, 2011, at 4:47 PM, Mark S. Miller wrote:

The whole thread is at

esdiscuss/2008-October/thread.html#7844

Please see Jon Zeppieri's reply in particular, quote:

I think the real problem here is that the <updateExpr>, as written by the user, is (or, rather, usually is) an assignment, and you should give the user what the user asked for.

I also find it odd that

{ let x = 0; for (; x < n; x++) ... }

should have different behavior than

for (let x = 0; x < n; x++) ...

-Jon


This is a valid criticism.

It is, and you're right, it's not the same as the one I tried to get away with below:

The for(;;) loop is unstructured, the update part after ; really is any

mutating expression you want (or a pure no-op, even). Specializing only let binding updates of certain forms (++, +=, -=, *=, what else? How about calls to functions that capture the loop variable and mutate it?) is too subtle and special. That's why I call it "wrong".

This is not. My desugaring does not descend into, special case, or transform the updateExpr. Any Expression may appear here. In fact, as far as my desugaring is concerned, any Statement could appear here.

But Jon's post, a bit earlier, pointed out how your desugaring did not handle all cases. This led me to think you weren't trying to handle them all. Sorry about that!

Let's not get hung up on the for(;;) loop. It is an imperative construct.

I don't feel strongly about this -- it's just an "it would be nice if we could fix this." If not, I won't shed any tears.

No tears, no fears. Jon's point about pulling the let out before the loop changing things seems solid. I won't try to generalize it much, but here's an attempt:

So far, prior to 'let', JS's C-style

for (init; cond; update { body }

has been what it is in C: sugar for

init while (cond) { body update }

This implies not coupling the parts in new ways, such as a fresh-per-iteration binding when init is "let i = 0" that is initialized only the first time by that " = 0" initialiser, and somewhat magically initialized subsequently from the previous iteration's post-update value of the binding.

See what I mean?

I do. This is why Jon's criticism is valid.