archives

« Bugzilla Issues Index

#3007 — for..of throws away final `return` value from generator


The semantics for this kind of loop are well known:

for (var v=0; v<4; v++) {
console.log(v);
}
// 1 2 3

console.log(v); // 4

IOW, the `v` gets the final update of value, and then the loop terminating condition is consulted, and terminates the loop. But `v` has the value that it had at time of termination of the loop.

-----

However, the analog of this semantic does not hold for generators iterated by the `for..of` loop:

function* foo() {
yield 1;
yield 2;
yield 3;
return 4;
}

for (var v of foo()) {
console.log(v);
}
// 1 2 3

console.log(v); // still `3`, not `4` :(


I understand why the loop needs to terminate the way that it does. I also understand that the `return 4` isn't technically required to send its value out to a `next(..)` call.

However, in the case where we have an implementation that *does* send it out, like the current browser JS engines do, it would be nice if that value could be assigned to the `v` iteration variable **before** the terminating condition (`done:true`) is consulted to stop the loop.

That way, the final returned value `4` is not thrown away in the `for..of` loop case in the same way that a manual `.next(..)` call (in a supporting browser) would have given you the `4` and not thrown it away.

-----

Since I doubt there's a way (is there?) for `for..of` to tell the difference between a generator which has no `return`, has just `return;` or has `return undefined`, another result of my suggestion is:

function* foo() {
yield 1;
yield 2;
yield 3;
// no return here!
}

for (var v of foo()) {
console.log(v);
}
// 1 2 3

console.log(v); // undefined

IOW, if the generator has no (non-undefined) `return`, the variable `v` would always be `undefined` at the end of the `for..of` loop.

But I don't think that's such a bad thing, because:

1. Most people will be using `let` anyway, so they wouldn't expect `v` outside the loop.
2. If you use `var v`, and handle `v = 3` state in the loop, it's rarer that you want to ALSO handle the same `v = 3` state outside the loop.


This is going to be problematic for destructuring contexts, because it will require the final value to be destructible (= to be an object). Or do you suggest to apply the proposed change only when the assignment is not a destructuring assignment?


(In reply to comment #1)
> problematic for destructuring contexts, because it will
> require the final value to be destructible (= to be an object).

Do you mean:

function* foo() {
yield [1,2];
yield [3,4];
return [5,6];
}

for (var [x,y] of foo()) {
console.log(x, y);
}
// 1 2
// 3 4

console.log(x, y); // 5 6


I would expect that should work, right? Is there some reason that couldn't work?

------

Furthermore, this seems logical to me, though it might require a special case handling to swallow the error only in that final terminating case:

function *bar() {
yield [1,2];
yield [3,4];
}

for (var [a,b] of bar()) {
console.log(a, b);
}
// 1 2
// 3 4

console.log(a, b); // undefined undefined


> Or do you suggest to apply the proposed change only
> when the assignment is not a destructuring assignment?

That seems like it would be a troublesome inconsistency. I think (as above) I only suggest the special case handling if there's no final object to destructure.


------

Or... maybe there's no special case error swallowing, and [a,b] just errors like it would any other time you use destructuring against an incompatible return value. That'd be a tiny bit annoying that you needed a `try..catch` or to add a final `return []` in your generator, but it seems like a small tax to me compared to the greater consistency.


The `for` loop analogs:


function incXY(x,y) { return [x+1,y+1]; }

for (var x = 0, y = 0; x < 3; [x,y] = incXY(x,y)) {
console.log(x,y);
}
// 0 0
// 1 1
// 2 2


Works fine, right? However, this seems like it would be expected to error, so exception is the "right" thing to do:


function incXY(x,y) {
if (x < 2) {
return [x+1,y+1];
}
}

for (var x = 0, y = 0; x < 3; [x,y] = incXY(x,y)) {
console.log(x,y);
}
// 0 0
// 1 1
// 2 2
// TypeError: undefined is not an Object


Can't imagine anyone would complain that the error here is "wrong". Maybe same reasoning should apply to destructuring + `for..of` + generator-with-no-return-value.


Lets keep it simple. We do not need to optimize for this case. The common case is that you do not depend on the variable outside the loop and we should just optimize for performance in these cases.

Another point for not doing this is that we want to be in a world where people only use let so in that case the extra update would never be observable.


(In reply to comment #2)
> (In reply to comment #1)
> > problematic for destructuring contexts, because it will
> > require the final value to be destructible (= to be an object).
>
> Do you mean:
>
> function* foo() {
> yield [1,2];
> yield [3,4];
> return [5,6];
> }
>
> for (var [x,y] of foo()) {
> console.log(x, y);
> }
> // 1 2
> // 3 4
>
> console.log(x, y); // 5 6
>
>
> I would expect that should work, right? Is there some reason that couldn't
> work?

It means a shift in the importance of return-statements in generators.
Currently return-statements are barely needed/useful in generators, this may
make them mandatory. (More below.)


>
> Furthermore, this seems logical to me, though it might require a special case
> handling to swallow the error only in that final terminating case:
>
> function *bar() {
> yield [1,2];
> yield [3,4];
> }
>
> for (var [a,b] of bar()) {
> console.log(a, b);
> }
> // 1 2
> // 3 4
>
> console.log(a, b); // undefined undefined

So basically different execution paths based on the final iterator result value
(`undefined` vs. non-`undefined` result values).


> > Or do you suggest to apply the proposed change only
> > when the assignment is not a destructuring assignment?
>
> That seems like it would be a troublesome inconsistency.

Agreed!


> Or... maybe there's no special case error swallowing, and [a,b] just errors
> like it would any other time you use destructuring against an incompatible
> return value. That'd be a tiny bit annoying that you needed a `try..catch` or
> to add a final `return []` in your generator, but it seems like a small tax to
> me compared to the greater consistency.

Both alternatives don't look too compelling. I can't imagine there will be any
support to make try-catch a de facto requirement around every for-of loop with
destructuring. The other alternative requires to have a matching empty element,
which acts as a dummy value. And depending on the data types, such an element
may not even exist. Granted, you can move the destructuring operation into the
loop body to avoid these problems, but then again you can also perform a manual
iteration to retrieve the final iterator result value.


Also: for-in loop iteration uses the same mechanics as for-of loops, but I
guess you don't want to change for-in loop semantics?
---
for (var pk in {prop: 0}) print(pk);
print(pk);
// "prop" "prop"
// or: "prop" undefined ?
---


I agree that trying to special case outer scope destructuring assignments on loop completion would be pretty arbitrary.

But if may make sense for the final iterator value to be the normal completion value of the for-of statement (in the "completion reform" sense)


(In reply to comment #4)
> It means a shift in the importance of return-statements in generators.
> Currently return-statements are barely needed/useful in generators, this may
> make them mandatory. (More below.)

I hear conflicting statements about the importance of them. I'm dubious that there's any defensible position which says either "they are important" vs. "they are not important". Ergo, they are important because they are important to some.



> So basically different execution paths based on the final iterator result value
> (`undefined` vs. non-`undefined` result values).

Not necessarily different paths. In a JS sense, a `try { x = final_value } catch(e){}` -- just a suppression and throw-away of the error if the final de-structuring assignment fails -- seems like it would be sufficient.



> Also: for-in loop iteration uses the same mechanics as for-of loops, but I
> guess you don't want to change for-in loop semantics?
> ---
> for (var pk in {prop: 0}) print(pk);
> print(pk);
> // "prop" "prop"
> // or: "prop" undefined ?

The important difference is that a `for..in` loop completely exhausts all enumerable keys in the object, so there's no final `undefined` value to overwrite `pk` with.

Moreover, the `for..in` loop doesn't omit the final property (as `for..of` does purely for the sake of making it work sensibly with generators -- see this twitter conversation: https://twitter.com/awbjs/status/453262038676946945)

What I am suggesting is that `pk` or `v` or whatever is always the last value from some iteration with `for..of`, just like it is with `for` and `for..in`.

In the case of a normal `for` loop, it's the final value that triggers the terminating condition. In the case of `for..in` property enumeration, it's the final key enumerated. In the case of `for..of` iteration over a normal data structure, it's the final value in that data structure.

So, in the case of `for..of` over a generator (the odd one out), it should be the final `return` value (whatever it is, even if it's omitted).

That's what I mean by arguing for consistency here.


> In the case of `for..of` iteration over a normal data
> structure, it's the final value in that data structure.

Actually, I was a bit unclear/incomplete here. Let me clarify.

What I'm saying is, because generators have this special behavior (ability to return a value "at/after completion") that's not true of regular data structures, the special casing behavior should apply only when `for..of` is used on a generator, not when `for..of` is used on normal data structure iterations.

For example:

for (var v of [1,2,3]) {
console.log(v);
}
// 1 2 3

console.log(v); // 3

The array iterator's `value:undefined` doesn't (and shouldn't) overrwite `v`, because there's no possible way for that array iterator to do `return` and send along another value (that wasn't in the array enumeration) with `done:true`.

But since generators *can*, and many do, send along another return value with `done:true`, then we need to get that value out, and not swallow it.


Actually, upon further reflection, I completely disagree with myself in that previous comment. Wish I could delete it. :P

So here's what I actually meant and intend:

Any `for..of` loop will be using an iterator. That might be an iterator of a generator, it might be the built in Array[@@iterator], or it might be your own custom iterator.

But in any case, since iterators *can* return values along with `done:true`, `for..of` should never discard those, regardless of what kind of structure the iterator is attached to.

And to answer André's question about `for..in` from earlier, the reason `for..of` should behave differently to the `for..in` is because `for..in` isn't using an iterator, so there's no way that `pk` could ever get overwritten. If there ever was a way to define your own enumerator for `for..in` loops, and if that *thing* let you return values with the termination signal, then `for..in` would need to act like `for..of`. For consistency sake, obviously.


for (var v of [1,2,3]) {
console.log(v);
}
// 1 2 3

console.log(v); // undefined

// --------------

function* foo() { yield 1; yield 2; yield 3; }

for (var v of foo()) {
console.log(v);
}
// 1 2 3

console.log(v); // undefined

// --------------

var customObj = {
[Symbol.iterator]: function(){
var __v = 0;
return {
next: function(){
if (__v < 3) return { value: ++__v, done: false };
return { value: "foobar", done: true };
}
};
}
};

for (var v of customObj) {
console.log(v);
}
// 1 2 3

console.log(v); // foobar

// --------------

function* customGen() { yield 1; yield 2; yield 3; return "foobar"; }

for (var v of customGen()) {
console.log(v);
}
// 1 2 3

console.log(v); // foobar


(In reply to comment #6)
> (In reply to comment #4)
> > It means a shift in the importance of return-statements in generators.
> > Currently return-statements are barely needed/useful in generators, this may
> > make them mandatory. (More below.)
>
> I hear conflicting statements about the importance of them. I'm dubious that
> there's any defensible position which says either "they are important" vs.
> "they are not important". Ergo, they are important because they are important
> to some.

The eight (or eleven when counting general availability) time gap between PEP 255 and PEP 380 may show that return statements are not one of the most important features in generators.


> > So basically different execution paths based on the final iterator result value
> > (`undefined` vs. non-`undefined` result values).
>
> Not necessarily different paths. In a JS sense, a `try { x = final_value }
> catch(e){}` -- just a suppression and throw-away of the error if the final
> de-structuring assignment fails -- seems like it would be sufficient.

Exception suppression is way too error prone to be applicable in this case. Too many side effects are possible during destructuring.



> > Also: for-in loop iteration uses the same mechanics as for-of loops, but I
> > guess you don't want to change for-in loop semantics?
> > ---
> > for (var pk in {prop: 0}) print(pk);
> > print(pk);
> > // "prop" "prop"
> > // or: "prop" undefined ?
>
> The important difference is that a `for..in` loop completely exhausts all
> enumerable keys in the object, so there's no final `undefined` value to
> overwrite `pk` with.

For-in loop iteration sure does omit the final iterator result value. See 7.4.8, 9.1.11, 13.6.4. TLDR: The final iterator result is `{done: true, value: undefined}`.


> In the case of a normal `for` loop, it's the final value that triggers the
> terminating condition. In the case of `for..in` property enumeration, it's the
> final key enumerated. In the case of `for..of` iteration over a normal data
> structure, it's the final value in that data structure.

C-style for loops enable greater configurability over their termination condition, so the term "final value" may not even apply here.


(In reply to comment #8)
> And to answer André's question about `for..in` from earlier, the reason
> `for..of` should behave differently to the `for..in` is because `for..in` isn't
> using an iterator, so there's no way that `pk` could ever get overwritten. If
> there ever was a way to define your own enumerator for `for..in` loops, and if
> that *thing* let you return values with the termination signal, then `for..in`
> would need to act like `for..of`. For consistency sake, obviously.
>

This is already possible:
---
function* g() { yield "a"; yield "b"; return "c"; }

// Prints: a b
for (var k of g()) print(k);

// Prints: a b
for (var k in new Proxy({}, {enumerate: g})) print(k);
---


> For-in loop iteration sure does omit the final iterator result
> value. See 7.4.8, 9.1.11, 13.6.4. TLDR: The final iterator
> result is `{done: true, value: undefined}`.
>
> If there ever was a way to define your own enumerator for `for..in`
> loops, and if that *thing* let you return values with the termination
> signal, then `for..in` would need to act like `for..of`. For
> consistency sake, obviously.
>
> This is already possible:
> function* g() { yield "a"; yield "b"; return "c"; }
> for (var k in new Proxy({}, {enumerate: g})) print(k);

OK, I stand corrected.

I'd argue you should change `for..in` to behave the same way I'm suggesting `for..of` should work. But I'm pretty sure the answer will be: "we can't break existing code."

Of course, you don't have the same argument against changing `for..of`.

What you've proven is that it's "hard" (complexity, trade-offs, etc) to do what I'm suggesting. I'll grant that.

Whether it's "hard" or not, I think consistency/coherence (in the mental/teachability sense) should be an important goal of designing such mechanisms.

One conclusion of such problematic design is "avoid `for..of` iteration if there might be a terminating return value that you care about." This is nonsensical, because if I'm just consuming some iterator, how am I supposed to know such a detail?

So turning the situation around, you could say "don't design an iterator that returns terminating values", or even "terminating return values from iterators should be disallowed".

This thread rejects that premise, preserving (and I would argue, endorsing) terminating return values: http://esdiscuss.org/topic/proposal-generator-returning-a-value-should-throw-syntaxerror

So we're left with "don't design an iterator with a terminating return value if someone may consume your iterator with a `for..of` loop." This is equally nonsensical, because I have no control over how someone consumes my iterator.

In other words, we're pitting iterator-design against iterator-consumption, and intentionally leaving this footgun gap where if those two don't match, information loss occurs.

I think this intolerable design. But I don't really have any more to say on the topic. So I'll leave it to the rest of the discussion to adjudicate.


(In reply to comment #11)
Your key point seems to be there is no way to access the final "return" of an iterator invoked using a for-of. That's certainly true with the current design and their isn't an obvious way to extend for-of, in all of its forms, to expose the return value. That doesn't seem so bad because if you care about that value you can code the loop in other ways. For example:

let itr = collection.values().
let next;
while (next=itr.next(), !next.done) {
let v = next.value;
//loop body
}
console.log(next.value) //the return value from the iterator.

However, like I said in Comment #5, it may make sense to set the completion value of the for-of statement to the final "return" value of the iterator. If we do that and eventually add do expressions to the language you will someday be able to write the loop in Comment #0 like this:

let ret = do { for (v of foo()) console.log(v)}// logs: 1,2,3
console.log(ret); /4

I'll further explore whether changing the for-of completion value seems reasonable. I don't think we will go down the other path of trying to assign to the loop variable.


Not going to change the completion values of for-of. As it stands right now, all control structure statement are consistent in returning the value of the last block they evaluated (or undefined if none)

The bigger problem of access the iterator result object is something we can't address for ES6.