lexical for-in/for-of loose end

Allen Wirfs-Brock allen at wirfs-brock.com
Fri Feb 3 16:13:31 PST 2012

On Feb 3, 2012, at 12:44 PM, Jason Orendorff wrote:

> On Fri, Feb 3, 2012 at 11:23 AM, Allen Wirfs-Brock
> <allen at wirfs-brock.com> wrote:
>> On Feb 3, 2012, at 6:42 AM, Jason Orendorff wrote:
>>>    for (let V in EXPR) STMT
>>>    for (let V of EXPR) STMT
>>>    for (let V = EXPR; ...; ...) STMT
>> the third case is different in several ways. Including:
>>   let V =EXPR is a distinct syntactic pattern that has a specific semantics  that requires that the EXPR is evaluated in the same scope as EXPR. However, in most cases (but consider |et x=(x=5,++x);) all of EXPR will be in V's temporal dead zone.
>>   The actual syntax is for (let V1=EXPR1, V2=EXPR2,...;...) STMT Again, the semantics for such declarations is that all of the Vs are defined in the same scope and EXPRn+1 is outside the TDZ for for Vn.
>> Using your semantics for the 3rd form would mean that its let clause was not consistent with let declarations in all other contexts.
> The behavior of let-declarations is that the name hoists to the
> enclosing block. We don't want to be *that* consistent.
> So the situation ends up being: we will have a statement that looks
> like "for(let V = ...)". It can either be partly consistent with other
> statements that look like "for (let ...)" or partly consistent with
> other statements that look like "let V = ...".

I think that the best way to think about this is that all for statements with let/const declaration implicit introduce a block to contain those declarations.  The base question here is the extent of that block. Does it surround the entire for statement,  or just the statement part of the for statement.  Or does it need two blocks, one surrounding the for and one surrounding the statement. 

The |let| is "inside" the |for| so I think |for| bounded hoisting is fairly consistent with general let declarations.

> Either choice breaks with consistency on one side; TC39 should pick
> the way that astonishes the fewest people.
>>> What is even more important than consistency is that the language work
>>> for people. We know that *not* having per-iteration bindings
>>> astonishes users, because that's how SpiderMonkey does it, and people
>>> are regularly astonished.
>> I think you are over generalizing from a specific use case.  This bug cuts both ways.

Just to reinforce something I think I already said.  I'm not questioning the desirability of  per iteration bindings for for-in/for-of.  I'm saying that in some cases (not most) that people who use the fully generality of the for(;;) statement will be astonished.  I think this is even recognized by you in your desugaring by the fact that you use %tmp to propagate the value of V from one iteration to the next.  It would be astonishing that a V++ in the loop body did not change the value of V seen by the next iteration.

That desugarings work fine in that regard as long as there is no closure capture involved. But consider:
   for (let keepGoing=true, stopper=function() {keepGoing=false}; keepGoing;) {
      let thisOne = getNext();
      if (thisOne == null) keepGoing = false;
      else thisOne.scanForSomeSpecificConditionAndIfDetectedDo(stopper);

There are lots of other ways to structure such a loop. But given the observable value propagation from iteration to iteration, it isn't unreasonable for a programmer to expect this to work.  They will be surprised that it doesn't.

> Could you explain a little more why you think that's the case?
> Grepping finds 1,227 loops of the "for (let ...; ...; ...)" variety in
> Mozilla's codebase. Of these 95% are simple counting loops, and *none*
> contain functions in the loop-head. I estimate about 4% of these 1,227
> loops have functions in the body, most of which escape, but it is
> harder to put precise numbers on that. Loops where the loop variables
> are modified in the loop body seem rare (I didn't see any), but I
> can't say how rare.

It might also be useful to look at for(var ...;...;...) and for (...;...;...) loops.  Nobody is proposing changing the scoping for those, but they might provide a broader view of how people use the generality of for(;;) loops

> Of the non-simple-counting loops, here is a sample:
>    for (let child = element.firstChild; child; child = child.nextSibling) ...
>    for (let frame = aElement.ownerDocument.defaultView; frame !=
> content; frame = frame.parent) ...
>    for (let row; (row = aResultSet.getNextRow());) ...
the above is a little odd
>    for (let i = 0; this[i] != null; i++) ...
> In all these cases, per-iteration bindings would do the right thing.
> If the choice comes down to astonishing this programmer:
>   for (let i = 0; i < n; i++) {
>       buttons[i] = makeButton();
>       buttons[i].click(function () { alert("pushed button " + i); });
>   }

I would actually feel a lot more comfortable with per iteration binding of for(;;) if the for let declaration was limited to a single binding.  In that case, a programmer really has to go out of their way to closure capture the outer for bindings. I'd have no problem with saying that my above example would have to be written as:

{ let let keepGoing=true; stopper=function() {keepGoing=false};   
   for (; keepGoing;) {   //aka while
      let thisOne = getNext();
      if (thisOne == null) keepGoing = false;
      else thisOne.scanForSomeSpecificConditionAndIfDetectedDo(stopper);

> or astonishing this one:
>    let geti;
>    for (let i=(geti = function() {return i},expr), expr, incr =
> function(i++), decr=function(i--), cmp=function(){return i<n};
> cmp();incr()) {
>        let j=i;
>        i +=10;
>        decr();
>        ...
>        If (j+-9 !== i) ...
>        ...
>    }
> then from where I'm sitting it looks like an easy choice. Let's
> support realistic use cases.
> Of course if per-iteration bindings would be confusing in practice,
> that would be bad. But the for-loop head issue should not be a major
> consideration, because people truly almost never write code like that.

But I also have to validly (and hopefully reasonably) specify exactly what happens for the unrealistic use cases.  There is a problem with your desugaring in that the evaluation of INIT isn't scoped correctly relative to V. Mark's, doesn't have this problem, but I'd still have to specify something at least semi-sane for any closures in INIT.

> I'd be glad to have range(); however I don't think it's effective to
> address usability issues by offering more alternatives. :-\
> -j

More information about the es-discuss mailing list