a weird yield* edge case
From my point of view, it should do nothing if there is no throw() method to call. The problem you are pointing is worthy to be noted but is a problem for the implementer.
Another option would be to throw. Then the caller can tell that they did something that was not expected by the inner iterator.
On Sat, Jan 31, 2015 at 8:56 AM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
Another option would be to throw. Then the caller can tell that they did something that was not expected by the inner iterator.
Yes, this makes sense to me. The code is violating the implicit contract; this should be allowed when it's harmless, but when you actually run into a situation that's problematic, throwing to the top-level is reasonable.
On Jan 31, 2015, at 8:56 AM, Erik Arvidsson wrote:
Another option would be to throw. Then the caller can tell that they did something that was not expected by the inner iterator.
I like that solution!
I do as well. What exactly is thrown - the input to the generator's throw method?
Don't think so. The throw method argument is what would have been thrown if the inner iterator actually had a default throw method.
It should probably be a new TypeError, then it's implementation defined text can be something describing the protocol violation.
But this perhaps leads to rethinking what happens when for-of invokes the throw method on its iterator. The plan of record, which the spec. currently reflects, is that if the exception that comes back from the 'throw' method is different from the one that was originally passed in as its argument then the original exception is the one that is propagated out of the for-of. (There are two possible exceptions that could be propagate in this case, so we have to pick one and drop the other one).
The logic behind that choice, was that the original exception produced by the loop body, is more likely to be the one that the surrounding code was prepared to deal with. But in the case we are considering, this choice would obscure the fact that the inner protocol violation had occurred. From a debugging perspective. it would seem better to propagate the final exception rather than the original exception. That would enable a developer to debug backwards starting from the final exception. That model seems more likely to make bugs visible and get them fixed.
I think that people would generally will expect yield *
to be have like
yield inside a loop. So most people would expect:
yield* iterator
To behave like:
for(let val of iterator){
yield val;
}
While I'm not (yet) suggesting that the behaviour should be similar in both cases this is definitely something to consider before deciding on throwing or suppressing.
But this perhaps leads to rethinking what happens when for-of invokes the throw method on its iterator.
Having trouble finding this - what is the section number?
On Jan 31, 2015, at 10:53 AM, Kevin Smith wrote:
But this perhaps leads to rethinking what happens when for-of invokes the throw method on its iterator.
I should have said, "when for-of" invokes the close method, because of an exception and the close produces a different exception.
Having trouble finding this - what is the section number?
13.6.4.14, everyplace where IteratorClose is called.
The actual selection of which exception is make in 7.4.6 IteratorClose, steps 7 and 8
On Jan 31, 2015, at 10:13 AM, Benjamin (Inglor) Gruenbaum wrote:
I think that people would generally will expect
yield *
to be have like yield inside a loop. So most people would expect:yield* iterator
To behave like:
for(let val of iterator){ yield val; }
While I'm not (yet) suggesting that the behaviour should be similar in both cases this is definitely something to consider before deciding on throwing or suppressing.
They are similar, but the transparency requirement of yield* has priority It's not a pure desugaring. For example, yield* passes through the IteratorResult objects produced by the inner iterator. The above desugaring can't do that.
Oh, I completely agree that yield *
is not just desugared into the above
for...of loop form. I'm just saying that that's how most developers will
likely look at it. Adding a while loop that calls the inner iterator's next
with the same value wouldn't change what developers would expect here.
I'm not arguing for one solution or another - just making the point that if it'll be anything like python's yield from - when developers think of yield* they'll think of pumping an iterator from another iterator and expect the behaviour of throw to be similar.
I should have said, "when for-of" invokes the close method, because of an exception and the close produces a different exception.
Ah yes. I think those semantics, as currently specified, are right on.
So what would we expect an inner iterator to do, if it supports "return" but not "throw"? What would an equivalent "throw" method, in such a situation, look like?
If it doesn't have a "throw" method then that means that it cannot usefully process errors. One generator function which fits that description might be:
function* a() {
try {
yield 1;
} finally {
print("cleanup");
}
}
If we call "throw" on a generator produced by this function (when the generator is paused within the try block), the finally block will run and then the thrown exception will propagate back to the caller.
So it seems to me that not having a throw method should be equivalent to something like:
throw(value) {
var r = this.return;
if (r !== undefined)
r.call(this);
throw value;
}
But I can see the argument for a TypeError (and not calling "return") as well.
On Feb 2, 2015, at 7:26 AM, Jason Orendorff wrote:
On Sat, Jan 31, 2015 at 10:56 AM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote: Another option would be to throw. Then the caller can tell that they did something that was not expected by the inner iterator.
I agree. In fact, isn't this required by transparency?
Inner().throw() in this situation would throw a TypeError (since there's no such method). So wrapper().throw() should throw a TypeError.
Except that a consumer (such as for-of) is expected to check for the existence of 'throw' before trying to invoke it. That's really where the transparency is lost in this case.
But I agree that throwing to indicate a "protocol violation" seems like a good approach. but it could mean that Inner doesn't get properly "closed". How how about this variation:
2a) An TypeError exception is thrown, but first 'close' (if it is present) is invoked on the yield* target iterator.
That would seem to both satisfy the 'close' contract (a consumer (in this case yield*) calls it if is terminating its use of an iterator before the "done" state is reached) and produces an exception to the wrapper consumer for the "throw" protocol violation.
I don’t think we should be assuming that an abnormal completion should run anything like the same logic as a normal completion. If I understand correctly, that’s what any proposal to fall back to “close” does.
"close" exists to deal with abnormal loop completion (or consumption completion, in general). A for-of loop only class "close" on its iterator, if something happens (exception, return, break) to terminate the loop before its iterator is exhausted. That is exactly what is happening if yield* throws because of a "protocol violation" for the situation we are talking about. yield*, just like for-of, doesn't call "close" on a normal (the inner iterator was exhausted) completion.
Generally, we want yield* to be as transparent as possible, so that if we have a Iterable Inner and a generator defined like:
function *wrapper() { yield *Inner(); }
any sequence of next/throw/return method calls on an iterator produced by wrapper will generate the same results as if the same sequence of method calls had been directly applied to the Inner iterator instance that is wrapped.
And this generally works (or at least will after I finish fixing some bugs) if wrapper and Inner are both generators. But here is the edge case where it appears impossible to achieve full transparency.
Generators always[*] have both a
return
andthrow
method and everything works transparently as expected. However, if the iterator returned by Inner is not a generator object, it might not have a "throw" method but still have a "return" method. Normally a "throw" to such a wrapper iterator would be forwarded byyield*
to the inner iterator's throw method, presumably triggering any "finalization" processing needed by the inner iterator. However,yield*
can't forward the the throw to a non-existent innerthrow
method. But the existence of areturn
method is a strong hint that the inner iterator may have some "finalization" it should be given an opportunity to perform.It seems like, there are two plausible semantics for this case, each with an undesirable characteristic:
yield*
doesn't invoke anything on the inner iterator in this situation. Transparency is preserved, but the inner iterator may have "finalization" processing that never gets executedyield*
invokesreturn
on the inner iterator in this situation. Inner iterator gets a chance to run its "finalization" processing. But full transparency is lost as a method is invoked on the inner iterator that that would not have been invoked with the wrapper wasn't sitting in the middle.(Note that wrapper already isn't truly transparent, because it is exposing a "throw" method that doesn't exist on the iterator iterator. It's possible, that the visibility of the "throw" method is what caused the consumer to invoke it, rather than invoking the "return" method.)
So, opinions on which of these alternatives is better? Are there any others?
[*] this situation actually can occur when Inner is also a generator, but it requires over-riding the 'throw' method for that generator's instance with undefined.