jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.98k forks source link

No way to do ES6 'for .. of' loops for generators? #3832

Closed jagill closed 8 years ago

jagill commented 9 years ago

Given a generator

gen = ->
  j = 0
  while j < 9
    yield j++
  return

how do we loop through it via the javascript for .. of loops? In javascript, we'd do

for (i of gen()) {
  console.log(i);
}

The coffeescript for i in gen() gives the C-style for (_i = 0; _i < gen().length; i++) loops, and for i of gen() gives for (i in gen()). Is there some way to get for (i of gen()) that I didn't see in the documentation?

igl commented 9 years ago

Bad performance is not specific to the for of feature. All es6 features are not optimized and v8 will just bail out to the interpreter. Afaik it does that on functions including a try-catch too. ES5 features also took a long time to be optimized by V8.

Therefor the performance issue is less relevant than generally thinking about how breaking changes will be introduced into coffee in the future. TC39 is just starting and es7 and es8 are not too far off... They will certainly not stop advancing the language now like they did 15 years ago and if coffee wants to stay relevant it has to move on too. Hack or Break(+1)?

JHawkley commented 9 years ago

I tend to think the optimal syntax for using iterators/generators would be for ... using since it just sounds right and suggests the iterated object carries special meaning (IE: for n using fibonacci()), but that would introduce a new keyword. The next best thing would have to be for ... with since the with keyword is JavaScript reserved and is not used in CoffeeScript at all.

Neither of these syntax suggestions would interfere with legacy CoffeeScript code either.

This is all assuming you want to keep iterator syntax separate from array (for ... in) and object (for ... of) iteration syntax. It would seem to me to be a good idea to do so, as long as CoffeeScript is maintaining ES5 compatibility, so as not to require weird conditionals at every for ... of block to figure out what the nature of the object being iterated is.

Plus, ES5 could take advantage of iterators too; so long as the object being used follows the iterator protocol, a structure not unlike what was suggested by @bernhard-42 could be used to run through it.

bernhard-42 commented 9 years ago

Unfortunately for standard arrays in the current nodejs, iojs, Chrome and Firefox typeof arr[Symbol.iterator] is also "function". The performance of for of for standard arrays is really different across all platforms - from poor to good:

                                  for(;;)  while()  for of  iterator via while
nodejs 0.12.5                         4       4      363         367
iojs v2.3.1                           5       4       93          64
Firefox 38.0.5                      160     154       59         255
Chrome 43.0.2357.130 (64-bit) Mac   306     325      125         694

(see https://gist.github.com/bernhard-42/27836f4ce719de6bee3e)

For the nodejs world I would really not like to see ES6 for...of used for standard arrays for the time being. The penalty is just too big. Any conditional around for...of needs to take care to omit arrays, e.g.

if (!Array.isArray(ref) && (typeof Symbol != "undefined") && (typeof ref[Symbol.iterator] == "function"))

From a performance perspective not critical, but ugly ...

fa7ad commented 9 years ago

I'm probably not qualified enough to be anpart of thus discussion but here's my 2 cents

What if we had a special shebang-esque comment to put CS in ES2015 mode?

Something like #! es2k15

What I'm proposing is everything by default compiles to ES5 (discount the es6 features already implemented) but when the shebang-thing is present, things like for..of are compiled assuming ES2015 (compiled to for..of rather than a es5 for..in).

This -i think- would not break backwards compatibility and allow the es2015 users to use the parts they love about es2015 in CS without the need for new keywords, etc.

fa7ad commented 9 years ago

*anpart = a part (Typo)

nilskp commented 8 years ago

Bikeshedding here, but what about this (no new keyword):

for yield value in squares()
  alert value
dyoder commented 8 years ago

@nilskp I don't think would work, because that's already a valid expression for a function returning an array. You can't assume that it's returning an iterator.

nilskp commented 8 years ago

@dyoder is it valid syntax? I get Error on line 1: unexpected yield.

babakness commented 8 years ago

Coffeescript 2.0 should break backward compatibility and compile to ES6 & ES7 or become itself "backwards" from an era progressively ever behind us.

dyoder commented 8 years ago

@nilskp I stand corrected. :)

My brain parsed that as an expression and move right along. But of course that's an assignment so an expression isn't valid. So what you're suggesting is basically a for yield construct?

My only concern with that is semantic. Iterators aren't related to yield, which is associated with Generators. Generators, of course, are Iterable, but so are arrays and so forth.

nilskp commented 8 years ago

@dyoder My proposal was for generators specifically, which was the original subject. I haven't thought about Iterators in general.

blitmap commented 8 years ago

Maybe:

for next x in generator()
  console.log x

I'm not sure anyone here wants a new keyword, but I'd prefer this over for yield x... since it makes it very obvious we're going by the generated next value and not yielding something.

Considering that generators in Coffeescript are defined in the same way regular functions are: It seems like there's an effort to make creating and using generators as transparent as possible. Iterating over a generated iterator is done in an each()-like way, so it might make sense to have the identical syntax:

for x in generator()
    console.log x

Coffeescript could reference a utility function to detect if it should iterate by .length or by .done. Afaik in ES6 arrays have a defined iterator anyway. ([Symbol.iterator])

eachIter = (a, f) -> (isArray(a) and walkArray or walkIter) a, f

walkArray and walkIter would do what you expect. It'd be important to separate it into 2 different code paths so you're not testing whether to treat the 'array-like' as an array or an iterator on each iteration. (performance concern?)

samuelhorwitz commented 8 years ago

I'd like to voice support for for...from. I thought of this independently the other day and saw it way up the thread. There is already precedent for CoffeeScript to deviate from standard loop names and improve them.

Javascript had for (var key in obj). "For key in object" doesn't even make semantic sense compared to CoffeeScript's replacement: for key, val of obj. It does exactly what one would expect and "for key/value pair of object" makes way more sense.

Then, using for val in arr completely altering the native Javascript meaning of for...in was also was an improvement. "For value in array". Or optionally, for val, idx in arr also read as "for value and index in array". Both these read way better semantically. Finally, CoffeeScript exposes for key of obj which is actually full circle back to the only original native Javascript possibility: for (var key in obj).

So basically for...in and for...of were added to CoffeeScript to completely supersede the sad version of for...in that Javascript supported, even with completely disparate syntax.

Now Javascript finally has for...of and they choose to make it not even support proper enumeration of objects but just iterators. It's perfectly in line with previous decisions made by the CoffeeScript team to say "this syntax is garbage and we will make our own".

tl;dr Just like CoffeeScript's for...in and for...of make semantic sense in use, for...from makes semantic sense for an iterator or generator. You are saying "for every value I take from this iterator". When looping over an array you are taking every value in it. When looping over a collection of key/value pairs you taking every key and value of it. And when looping over an iterator that yields a new result each time you are taking values from it.

atg commented 8 years ago

I'm totally convinced by the rationale for for ... from. Having to state awareness the loop may (or may not) empty the thing being looped over, is definitely a feature.

I can't tell you the number of times I've accidentally tried to loop over the same generator twice in Python. Of course the second loop never executes because the generator is now empty! Having for...from to annotate which of my loops consciously support generators would be very useful.

GeoffreyBooth commented 8 years ago

This has been merged into master per #4355. Anyone up for writing some documentation?

joeytwiddle commented 8 years ago

Great stuff! :sparkles:

in a perfect language, you wouldn't have three different syntaxes for these loops — you would only have one

That is still an option. A universal for .. within .. loop that accepts the performance hit and determines the type at runtime?

vendethiel commented 8 years ago

That is still an option. A universal for .. within .. loop that accepts the performance hit and determines the type at runtime?