On Feb 4, 2012, at 12:55 PM, Brendan Eich wrote:

> Allen Wirfs-Brock wrote:
>> On Feb 4, 2012, at 9:49 AM, Brendan Eich wrote:
>>> I agree we want to capture the first-iteration bindings in any closures in 
>>> those declarators' initializers.
>> 
>> It isn't clear to me why capture first-iteration is abstractly any better 
>> than "capture a hidden second x".  In both cases, in most iterations of the 
>> loop, evaluation of any such captures is going to reference the "wrong" 
>> binding.
> 
> The argument is as follows:
> 
>  for (let i = 0, a = some.array, n = a.length; i < n; i++) { ... }
> 
> here we definitely want the a in a.length (n's initializer) to be scoped by 
> the head let -- to be the a declared by the second declarator.
> 
> Now consider a bit of eta conversion:
> 
>  for (let i = 0, a = some.array, n = (function(){return a})().length; i < n; 
> i++) { ... }
> 
> It would be quite wrong for the a captured by the anonymous function 
> expression to be other than the a binding declared and initialized 
> immediately to the left.
Yes, I support the general principal that that initializers of bindings capture 
to the left.  But the problem here is that conceptually the let is defining 
multiple bindings (one per iteration).  I don't think many people are actually 
going to understanding the details of the proposed semantics and their 
implications.  Since most uses won't involve closure capture, any of the 
proposed semantics that have per iteration bindings with forward value 
propagation are just going to do the "right thing".  That is good.  However, I 
doubt that someone who actually codes a function in a for(let;;) initializer is 
going to be thinking, "of course, this only captures the first iteration 
bindings".  

> It would be bad for the eta conversion to break equivalence (use a 
> block-lambda instead of a function expression for full TCP).

With the TDZ alternative I proposed, there  would still be equivalence for:
     for(let x=x;;)...;
and 
     for(let x={|| x}();;)...;

Both throw for accessing an initialized variable.

But you're right that equivalence is lost for 
     for(let x=n, y=x;;)...;
and 
     for(let x=n, y={|| x}();;)...;

Whether this is better or worse than the "wrong capture" issue complete depends 
upon the actual programmer intent.

> 
> 
>> From a user perspective, the main advantage I see for capture first 
>> iteration is that it has a slightly smaller window of wrongness.  The 
>> captures evaluated in the first iteration will reference the correction 
>> binding, while latter iterations reference the wrong binding.
> Users expect and even (now that they know, and Dart raises the ante) demand 
> that each iteration gets fresh let bindings. Any who do capture an initial 
> binding in a closure must know, or will learn, that it's just the first one, 
> which fits the model.

For the latter, I strongly suspect that they won't know and will be WTF 
surprised when they encounter it.  The saving grace is that this will probably 
be very rare, although its possible that the introduction of block lambdas 
might somewhat change that.  Just don't know...

> If this were really a footgun (I don't believe it is without actual evidence 
> from the field) we could try to ban closures capturing the initial bindings. 
> That ad-hoc restriction would be quite a wart. It doesn't seem warranted.
> 
My TDZ solution is such a restriction.  But I don't see how it is any more ad 
hoc than any of the other changes we are talking about here in order to give 
for(;;) per iteration bindings.  Its wartiness  actually seems small and is 
restricted  to a situation where the programmer probably is actually expecting 
C=style per loop rather than per iteration binding. 

>> From an implementation perspective, it is probably a bit simpler to not have 
>> the extra hidden binding for capture.
> 
> I don't think so. The unrolling I showed was to use a tail-recursive 
> block-lambda helper. But real implementations will do closure analysis and 
> optimization (flat AKA display closures, e.g.) and use branch instructions 
> for loops, jumps for breaks, etc. Having the first binding rib open a bit 
> earlier than subsequent ribs is (I think) a small or zero-cost issue.

My comment wasn't about the bind to first unrolling.  It was about the "extra 
hidden binding" alternative in the first list of alternatives and is probably 
also applicable to by TDZ alternative.

> 
>> I really don't like the first iteration is different semantics
> 
> Different how?

Different from subsequent iterations...

Take Jason's example

   for (let i = 0; i < n; ) {
       setTimeout(...closure using i...);
       if (shouldAdvance())
           i++;
   }

If somebody decided to abstract the increment:

   for (let i = 0; advance={|| i++}; i < n; ) {
       setTimeout(...closure using i...);
       if (shouldAdvance())
         advance();
   }

advance does what is intended if called on the first iteration, but not on 
subsequent one iterations. I'd soon get an error when I ran this than having it 
get stuck in a loop.

> Making the first iteration's binding initialization capture guaranteed errors 
> would be different semantics. Capturing the first iteration's bindings from 
> closures in their initializers is not "different" any more than having 
> initializers is "different". The initialization part of the for loop is 
> already special. It's not like the update part.
> 
>> and think we should think about the above alternative.
> 
> Eta equivalence matters. Given that we want n = a.length to use the a 
> declared to the left in the same for-head declaration, I don't see how we can 
> make closures in a right-ward initializer capture some outer binding.
> 
> Capturing an error-only binding would need evidence of the footgun not being 
> useful for shooting other things. We don't have such evidence, not by a long 
> shot.

Well, we don't have a lot of evidence for any of this discussion.  Does any 
C-syntax language currently implement for(;;) in the manner that is being 
proposed?
> 
>> However, such closure capture is very rare (could use of block lambda based 
>> patterns change that??)
> 
> I don't think so -- equivalences are stronger, not weaker or different, with 
> block-lambdas vs. functions, due to TCP.
That was the point I have had in mind.  While function expressions in for(;;) 
initializers are now rare, that might change with the use of block-lambdas
> 
>> so it may come down to judgements about implementation costs.  Is capture 
>> first going to be significantly easier to implement than my alternative 
>> scoping? The answer is obvious to me.
> 
> Did you mean "isn't"?
yes
> 
>> In either case an implementation is like to special case loops with closure 
>> capture in their initializers.
> 
> Varying a sketch Jason posted to the SpiderMonkey internals list:
> 
>    for (let V = INIT; TEST; UPD)  STMT
> 
> compiles to
> 
>        enterblock <V>
>        INIT
>        goto L2
>    L1: STMT
>        reenterblock
>        UPD
>    L2: TEST
>        iftrue L1
>        leaveblock
> 
> This is very close to how SpiderMonkey compiles for(let;;) already -- the 
> only new instruction is reenterblock, which exits the current block and does 
> the equivalent of enterblock <V> again.
> 
> <V> is an immediate operand, in SpiderMonkey a block object literal created 
> by the compiler. It holds all the bindings and their stack offsets, however 
> many declarators with or without destructuring occur after let.
> 
> I claim implementation is not the driver here. User expectations, esp. savvy 
> users who might make some practical or theoretical (testing) use of eta 
> conversion, matter more.
Actually, I'm  more concerned about un-savy users who won't really understand 
the full semantics, whatever we decide they are.

Allen


_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to