yield* desugaring

Andreas Rossberg rossberg at google.com
Mon May 13 11:08:21 PDT 2013


On 13 May 2013 19:24, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
> On May 13, 2013, at 2:07 AM, Andreas Rossberg wrote:
>> On 12 May 2013 21:29, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
>>> As now specified:
>>>
>>> 1) I specified yield* such that it will work with any iterator, not just generators. To me, this seems essential.  Otherwise client code is sensitive to other peoples implementation decision (that are subject to change) regarding whether to use a  generator or an object based iterator.  This was easy to accomplish and only requires a one-time behavioral check to determine whether "next" or "send" should be used to retrieve values from the delegated iterator and an behavior guard on invoking "throw" on the delegated iterator.
>>>
>>> 2) yield* invokes the @@iterator method on its expression to obtain the iterator. This means you can say things like:
>>>    yield * [2,4,6,8,10];  //individually yield positive integers <= 10.
>>
>> Is that a good idea? I'd be fine with it if you could transparently
>> generalise to iterators, but you can't. You need to special-case
>> iterators that are generators, and for them the semantics will be
>> quite different. For example, they will recursively handle sends and
>> throws to the outer generator, whereas for other iterators, where will
>> those even go? In a nutshell, what you are suggesting is to break the
>> iterator abstraction.
>
> Yes, it's a very good idea.  The easy way for an imperative programmer (there are a few of us in the world) to understand yield* is as a yielding loop over an iterator.  Very straight forward to understand rather than describing it as a "mechanism for composing generators" (from the wiki) which I had no idea what it meant until I carefully studied the desugaring.  At that point it because clear that it was just a yielding loop over an iterator  that for some reason was arbitrarily being restricted to being a generator.
>
> That restriction is what is breaking the iterator abstraction.

See my previous reply. I think there is some confusion here about the
direction of the abstraction.

> Could you clarify the special-case handling you have in mind? There is nothing in the wiki proposal desugaring of yield* that guarantees  that the delegated object is an actual generator.  All it requires is a "send" method (and, in that desugaring,  "close" plus "throw" if "throw" is actually invoked).

Exactly. But iterators don't currently have 'send' and 'throw'
methods. So you would want to do something different for
generator-like objects then you'd do for other iterators.


> Regarding recursive "sends" to an outer generator.  This shouldn't work, according to the wiki proposal.  When executing a yield* the outer generator must be in the "executing" state. Invoking an inner generator from an yield* via a "send" invocation still leaves the outer generator in the "executing" state.  If the inner generator invokes "send" on the outer generator the "send" will throw because the outer is already in the "executing" state.

The case I was talking about is simply this:

  function* g() {
    yield* [1, 2]
  }

  var o = g()
  o.send(undefined)
  o.send(5)  // what does this mean?

But I suppose the answer is that the sent value is just dropped on the
floor, as per the iterator expression interpretation you gave in the
other post. Makes sense, I guess.


> First why do we need "send" at all.  Why not simply allow an argument to be passed to "next"  (of course, it is already allowed) and leave it up to the generator implementation as to whether or not they pay any attention to it.   Clearly a client needs to be aware when they are using a generator that expects to receive a value back from yield so that fact must be documented in the public contract of that generator.  Once that is done, the client can use "next" as easily as they could use "send".  Of course, if people really like the name "send" we could also provide that method for generators with the meaning:
>     send(value) {return this.next(value)}

I happen to dislike the name 'send' a lot and would rather call it
'resume'. ;) But your suggestion of merging it with 'next' sounds
plausible as well.


> That leaves only "throw" as an issue.  Personally, I'd just make it part of the Iterator interface and provide an Iterator abstract class that provides
>    throw(exception) {throw exception}
> as the default "throw" implementation so most iterator authors don't even have to think about it. Short of that, I think having an explicit behavior check for "throw" in the yield* algorithm is a very small cost (that only arises if someone  actually invokes the "throw" method on the outer generator) and would take care of most common situation where "throw" is likely to be invoked on an iterator..

That might actually work. If we manage to truly unify iterator and
generator types then you got me convinced.

/Andreas


More information about the es-discuss mailing list