tc39 / proposal-generator-arrow-functions

ECMAScript proposal: Generator Arrow Functions
115 stars 5 forks source link

Syntax #2

Open chicoxyzzy opened 4 years ago

chicoxyzzy commented 4 years ago

Possible solutions

Arrow function syntax

// Irregular
() =*> ...

// not the same order as in regular generator functions
() =>* ...

// also wrong order
() *=> ...

// ASI hazard
*() => ...

Introduce new generator keyword for both function and arrow function

generator function() {}
const foo = async generator function() {};

class Foo {
  x = 1
  // No more ASI hazard!
  generator foo() {}
}

Previous discussions https://github.com/tc39/proposals/issues/216

SpadeAceman commented 4 years ago
// Irregular
() =*> ...

Actually, to me this looks like the best option - it could be informally known (and searched for) as an "arrow star" function, or a "starred arrow".

Trying to add yet another keyword to the language (gen or generator) seems like a tougher path forward.

whitelizard commented 3 years ago
// Irregular
() =*> ...

Actually, to me this looks like the best option - it could be informally known (and searched for) as an "arrow star" function, or a "starred arrow".

  • Having the star in the middle (=*>) makes it clearer that it's a single token, and not a sequence of adjacent tokens (*=>, =>*).
  • Arrow function syntax is already different enough from classic function syntax that I don't think the "irregular" nature of this idea should be considered a drawback.
  • Is it ugly? Well, beauty is in the eye of the beholder, and unfamiliar syntax always looks weird until you understand its purpose. I would argue that there's beauty in how concisely =*> communicates its intent, in harmony with the concise beauty of existing arrow function syntax.

Trying to add yet another keyword to the language (gen or generator) seems like a tougher path forward.

  • Existing generator functions already have the asterisk as their, ahem, "star" feature sweat_smile ...so that creates a gravity well around an asterisk token of some kind, pulling things away from another keyword.
  • Adding gen[erator] would introduce confusion about existing syntax for generator functions. I don't think attempting to change or "fix" existing generator function syntax with this proposal will help it succeed.
  • As others have mentioned, conciseness of expression is a critical feature of arrow functions. This again pulls things toward an asterisk solution, and away from another keyword (async should remain the one exception in this regard).

This is exactly what I thought from the absolute start of knowing about this discussion. BIG thumbs up!

I think that =*> is the better of all alternatives so far, of exactly the reasons stated above.

I could also add the view that this is actually "regular", in that the star * is inserted in the middle of the main syntax piece that makes something a function, that is: function() (you can not have a function without the parenthesis) and => (an arrow function does not need parenthesis in all cases).

So these are the different function variants then:

// single sync
function() {}
() => {}

// multi sync (generator)
function*() {}
() =*> {}

// single async (promise)
async function() {}
async () => {}

// multi async (async generator)
async function*() {}
async () =*> {}
ExE-Boss commented 3 years ago

I’m also in favour of =*>.

unsigned-wrong-wrong-int commented 3 years ago

I think =>* is better.

It's the same order as the other uses of *, and easy to remember.

=>* is readable enough, isn't it?

starred-arrow samples

pepkin88 commented 3 years ago

For what it's worth, in LiveScript, the chosen form was with * at the end, like this: ~>*. Personally, I prefer that variant over others.

vwkd commented 3 years ago

I really like the gen keyword from https://github.com/tc39/proposal-generator-arrow-functions/issues/2#issuecomment-544652561. It's similar to "async", and not too long to make "generator async function" fill half of a line even before any parameters.

I never liked the star *. It seemed like arbitrary syntax without any meaning. Star as in "special"? Star as in "super"? Now seeing that fitting this star into arrow function syntax is so hard, why not actually go with a keyword. Inconsistency, you say? Is delaying this proposal really worth it for accommodating ugly syntax (my opinion!) consistently? IMO it should have been a keyword since the beginning.

As for "gen". Someone said there is "no value in abbreviating". Of course there is. Otherwise it would have been "asynchronous" and not "async", right?

Well, they also claimed that async is a "well-known abbreviation" for asynchronous. I'm surprised they can accurately determine what is "well-known". How do they know? Do they know everyone? Honestly, hiding personal preference behind such a sweeping claim isn't what I'd call professional.

Another point you can ask yourself is: If you didn't already know what a generator is, would the full word "generator" help you understand and use it in any way? I doubt so. Let's go full circle. When I first saw a function * in code I thought I was tripping. What the heck is that? What does it do? So "gen" is more descriptive than a *, and "generator" is more descriptive than "gen". But neither help you understand what this thing is and how to use it. So if you need to look up documentation anyway, it might just as well be "gen". And after you know it, well you know it. Just as with the ugly *.

I personally like gen best. That's it. I hope this proposal can eventually overcome the syntax discussion as using it is much needed.

ljharb commented 3 years ago

It's not personal preference - the word you're looking for is "anecdotal". The way I know is because I've seen people use the abbreviation "async" for "asynchronous" quite frequently, predating async/await - node.js is full of APIs suffixed with "Sync", and the others are always referred to as "async", for example - and i have never once, not ever, seen someone use the abbreviation "gen" for "generator". It's quite possible you, or someone else, have seen this usage, and I'd love to hear about it - but it's not an unreasonable assertion. It's just not an objective one.

I completely agree with you that gen is way more descriptive than an asterisk, as is generator, and that either would have been a better choice than the asterisk in ES6.

alfnielsen commented 2 years ago

First: Why is this threat discussion about star or not? (replacement of star)

This should not be a discussion about generator functions in javascript in general, (I do agree that the `is not the best choice )* This is about gettinggenerator arrow functions` into the language, which I guess we all want (as fast as possible!)

Changing the star to something else, would be something that the "normal" generator function also should apply to, aka that discussion is another language threat proposal!

There are already a syntax in the language for generator method (whether people like that syntax or not!)

Syntax for a function is: (? are optional)

Syntax for an arrow function are: (with some special case like single return etc..)

To follow the same syntax as a normal function and not introduce multiple language syntax for the same functional logic, the star should be place as in a normal function aka:

I have a hard time undestanding why this is even up for discussion ?

(again changing the star is something I like to see, but it has nothing to do with getting generator arrow function into the language )

mvduin commented 2 years ago

Agreed, *( args ) => ... is the only syntax consistent with existing generators. The only argument against it (that applies specifically to this syntax and not to function*) is the ASI hazard.

I previously posted a comment about that in #8 but this is a better place for it:


ASI is the awful plague of Automatic Semicolon Insertion. The problem here is that ASI is only done if not inserting a semicolon before the next token would result in a syntax error, while in this situation

x = y
*(z) => {}

the parser will happily accept x = y * (z) before it hits the => at which point it would have to go back and insert the semicolon before the *. I don't know if this would be genuinely problematic, but it would be unlike existing ASI rules.

However, I think this ASI hazard can also just be ignored since there's no reason to ever put an arrow-function/generator expression at the start of an ExpressionStatement, since this is grammatically only permitted if the arrow body is followed by a comma or a semicolon and in either case the function is discarded.

Note for comparison that it's not even possible for an ExpressionStatement to start with a non-arrow function/generator expression, since attempting to do so result in it being parsed as a function/generator declaration instead:

function() {};
^^^^^^^^

Uncaught SyntaxError: Function statements require a function name

This despite the fact that putting a non-arrow function expression at the start of an ExpressionStatement would (unlike the arrow case) not be useless, since function(){}() is (horrifyingly) valid syntax for a function call (as long as you don't put it at the start of a statement).

 

BTW there's no grammatical ambiguity in 1 * () => {} since the literal 1 cannot be followed by an expression (other than a TemplateLiteral), nor can the * operator be followed by an arrow function expression for that matter.

Currently the only context where the grammar allows an ArrowFunction after a * token is as argument to yield*, which means yield *() => {} would be ambiguous with yield* () => {}. However, while the latter is currently syntactically valid, it is semantically nonsense and will always cause a runtime error (() => {} is not iterable), so it is safe to eliminate this interpretation and thus the ambiguity. I'd also be fine with disallowing both interpretations and require writing yield (*() => {}).


In conclusion, the ASI hazard seems like a complete non-issue to me.

pepkin88 commented 2 years ago

However, while the latter is currently syntactically valid, it is semantically nonsense and will always cause a runtime error (() => {} is not iterable)

But you could make it iterable with Function.prototype[Symbol.iterator] = function* () {}, so yield* () => {} will not always cause an error.

mvduin commented 2 years ago

But you could make it iterable with Function.prototype[Symbol.iterator] = function* () {}

Ew, that's so cursed. I'd sincerely hope there's noone out there who has actually done that, but I guess it would also be an option to just keep parsing yield *() => {} greedily as yield* () => {}. Making it a syntax error and requiring disambiguation by writing yield (*() => {}) or yield* (() => {}) would allow for a more helpful parse-time error rather than risk unexpected runtime behaviour, at the cost of a minor (and easily fixable) backwards incompatibility for code that makes Function.prototype iterable to use yield* on arrows, if there's any out there.

Luxcium commented 2 years ago

TBH, any of *=>, =>* or =*> look totally fine and clear to me.

if I « think outside the box » why can't we use =>>

mikabytes commented 2 years ago

if I « think outside the box » why can't we use =>>

"the psychedelic syntax" :)

dead-claudia commented 2 years ago

Regarding *() => {} having a potential ASI hazard, I do want to point out a couple things:

  1. I personally anticipate approximately nobody is going to try to make an expression statement out of a generator function literal. Forcing it to be wrapped in parentheses is okay IMHO, and encoding this in the grammar this is as easy as just adding a negative lookahead for * in the definition of an expression statement. Edit: A similar situation exists with yield * ( ) => { } (a case that could reasonably see actual use in the wild), but one could similarly require parentheses here to disambiguate (and also preferably require them for yield* (*() => {}) as well to avoid unintentionally preventing a potential future yield** expr construct).
  2. Class generator methods already have an ASI hazard that's nearly identical, and I find myself running into that a lot more as I generally code without semicolons. (This is part of what's led me to centralize all my property declarations above the constructor as a general practice, including bound methods.)

Personally, I feel the ASI hazard concern is a bit overblown for this.

Edit 2: I notice after I send this that it's restating much of https://github.com/tc39/proposal-generator-arrow-functions/issues/2#issuecomment-916079842. This does add a few more things to round out the idea, though.

conartist6 commented 1 year ago

One problem with all the => and > etc is that they look really nice in code fonts where is middle aligned but pretty weird in normal fonts. If you discount the idea that the syntax looks pretty because all the symbols line up perfectly, then () => {} seems like the more natural syntax to me.

conartist6 commented 1 year ago

BTW there's no grammatical ambiguity in 1 () => {} since the literal 1 cannot be followed by an expression (other than a TemplateLiteral), nor can the operator be followed by an arrow function expression for that matter.

It is ambiguous. 4 * function(){} === NaN, and must continue to.

mvduin commented 1 year ago

It is ambiguous. 4 * function(){} === NaN, and must continue to.

Ehm, my statement was about arrows, not about non-arrow function expressions. The current grammar does not syntactically allow an arrow function expression as operand to the * operator:

4 * () => {}
  ^^^

Uncaught SyntaxError: Malformed arrow function parameter list
dead-claudia commented 1 year ago

image

Just for quick reference. I reproduced it likewise in Chrome's dev tools.

My personal preference for this is still to require such yields to be parenthesized to disambiguate, which ultimately wouldn't result in changing the meaning of any currently syntactically legal productions.

sukima commented 1 year ago

This is already an old and long discussion but the goal stated in the beginning is still worth while. I'd like to refocus and offer my perspective. For me a generator arrow function is not about one-liners or easier to read semantic (though these are an added benefit for me) but the auto-binding of this. As it stands though regardless of syntax this really sucks:

let that = this;
function * foo() {
  that.nowWeHaveBinding
}

And in most cases where I wish I had an arrow-function generator is in that situation when I want to make a generator in my closure but have the this bound properly.

cowboyd commented 1 year ago

I'd like to add my $.02 and concur with @mvduin and @dead-claudia. In addition to the reasons they put forward, ubiquitous integration of linters, formatters, language servers, and particularly TypeScript makes the ASI hazard feel like it's receding from 2023 close to, if not past, the vanishing point.

The fact is that *() => is the only option that is perfectly aligned with intuition. As for gen, generator, etc... They are great in a green-fields, context free situation, but * is what we have, and * is what we will always have, so combining it with the existing arrow syntax in a manner consistent with all the other ways to declare a generator function is the only option that does not introduce a cognitive dissonance.

SpadeAceman commented 1 year ago

The fact is that *() => is the only option that is perfectly aligned with [my personal] intuition.

Fixed. 🙂

The fact is that my personal intuition regards function and => as the function signifiers, and expects the generator asterisk to be closely associated with them in some way (as is currently the case with function*):

In contrast, placing the generator asterisk next to the arguments list of an arrow function ((*() => {})) isn't intuitive to me, because it disassociates the generator asterisk from the function arrow.

Ultimately (assuming this proposal eventually moves forward), syntax will be selected that will require one of us to adjust our intuitions. I'm simply pointing out that intuition can be subjective, and explaining how I (and others here) have reasonably arrived at different syntax suggestions.

As for gen, generator, etc... They are great in a green-fields, context free situation, but * is what we have […]

On this point, we agree. 🙂

mvduin commented 1 year ago

The fact is that my personal intuition regards function and => as the function signifiers, and expects the generator asterisk to be closely associated with them in some way (as is currently the case with function*)

Except we also already have the *name(){} syntax for generator methods in classes and object literals, which lacks any explicit function-signifier.

conartist6 commented 1 year ago

@mvduin yes but that construct is understood to be purely a syntactic sugar for name: function*(){}.

There's no need to sugar the arrow version like that because you'd just write name: () *=> {} (or whatever syntax is adopted). That works basically as well as the sugared shorthand because in the modern version of the language the anonymous-looking function is actually named name.

cowboyd commented 1 year ago

Ultimately (assuming this proposal eventually moves forward), syntax will be selected that will require one of us to adjust our intuitions.

Agree. All the strange gnobbles of the original => syntax like () in relation to argument list and {} around body were things that everybody eventually seemed to move past, and this seems minor in comparison, so I would much rather see anything than nothing.

That said, I do think that given the full breadth of how functions are expressed in JavaScript, the bit that marks the function variant appears very much in the head position. I think this is where a lot of the appeal of the gen/generator proposals comes from. Viewed from this perspective, it's very much the original function* syntax that is the only real outlier when viewed along side a *() => and gen () => {}

Type Normal Async Generator (gen) Generator (*)
Short () => {} async () => {} gen () => {} *() => {}
Method method() {} async method() {} gen method() {} *method() {}
Dynamic [property]() {} async [property]() {} gen [property]() {} *[property]() {}
Long function () {} async function() {} gen function() {} function*() {} 🤔

Which is not to say that it isn't more intuitive in the individual context of a expressing a generator lambda , but it does seem to me that by not following this convention, you are introducing not just another sequence to parse and assign meaning to (=*>), but you're also going against the prevailing current of not placing function type at the head of the expression.

Type Normal Async Generator (gen) Generator (*)
Short () => {} async () => {} gen () => {} () =*> {} 🤔
Method method() {} async method() {} gen method() {} *method() {}
Dynamic [property]() {} async [property]() {} gen [property]() {} *[property]() {}
Long function () {} async function() {} gen function() {} function*() {} 🤔

Rather than "aligned with intuition", I think it is safer to say more narrowly that *() => {} is aligned with the existing forms of expressing functions.

dead-claudia commented 1 year ago

I still stand by my assessment that *() => ... isn't an issue.

It also neatly follows the existing patterns with generators, so people would know it right away.

ljharb commented 1 year ago

@conartist6 it is not purely sugar; concise methods can't be constructed (they have no .prototype) and they can use super.method syntax.

bathos commented 1 year ago

(minor nit: arrows can also use that syntax, but only lexically & depending on the outer env they close over. point remains of course!)

pepkin88 commented 1 year ago

you are introducing not just another sequence to parse and assign meaning to (=*>)

I personally wouldn't say it is a bad thing here. The symbol is derived from an existing one, it looks like a variant of =>. And with =>* or *=>, the arrow part is left intact.

With *() => {} you have an asterisk * first, then a parameter list, then the arrow. With =>* the whole indicator of a generator function is in one place.

Someone may say, that with async functions async and => are also separated. True, but the word async doesn't show up in other contexts with a different meaning. The * symbol is quite overloaded, primarily used for multiplication, and I feel that might be the reason, why I don't quite like the *() => {} syntax.

Another reason is the variant with one parameter: *param => { body } vs param =>* { body } The first one looks to me like it is the param that is modified by *, not the function.


but you're also going against the prevailing current of not placing function type at the head of the expression.

In my perception, this kind of already happened with all arrow functions, where instead of the word function, the indicator of "functionality" is an arrow => placed after parameters. So in my view, placing an asterisk on the right of the function indicator (=>) is a consistent choice for a generator function syntax: function* → =>*.


In the end though, whichever syntax will be chosen, people will get used to it.

cowboyd commented 1 year ago

It would be amazing to see some movement on this in one direction or the other. What would it take?

cowboyd commented 9 months ago

This feels part and parcel of a category of asymmetries that leave generators in the lurch. I find myself wanting higher order iterators more and more.

for yield* (let item of iterable) {}

or the like

dead-claudia commented 9 months ago

This feels part and parcel of a category of asymmetries that leave generators in the lurch. I find myself wanting higher order iterators more and more.

for yield* (let item of iterable) {}

or the like

@cowboyd You could just do nested for loops for that. Also, the consumption side of that has little to do with this bug.

cowboyd commented 9 months ago

This feels part and parcel of a category of asymmetries that leave generators in the lurch. I find myself wanting higher order iterators more and more.

for yield* (let item of iterable) {}

or the like

@cowboyd You could just do nested for loops for that. Also, the consumption side of that has little to do with this bug.

I agree, it has little to do with this bug. I'm more pointing out that there are a lot of asymmetries where generators are like second class citizens compared to async/await

gurgunday commented 6 months ago

Instead of discussing introducing new syntax like generator that would also need to work with the function keyword, let's focus on moving this proposal forward without affecting other things

What matters is, today, any of the following is understandable by someone who already knows that function* creates a GeneratorFunction object:

// Irregular
() =*> ...

// not the same order as in regular generator functions
() =>* ...

// also wrong order
() *=> ...

// ASI hazard
*() => ...

It's as simple as combining the token that creates a function with *

Let's confirm that the final syntax will be one of these to limit the scale of the discussion

Brendan Eich just reaffirmed his support for this proposal too, so there is no better time to get actual momentum 🤠🚀

Just to make sure, @hemanth, are you still championing it?

dead-claudia commented 6 months ago

I'm still not convinced the ASI hazard is a real issue with the *() => ... syntax. To avoid ASI ambiguity, just do a negative lookahead for * when parsing for expression statements (or in other words, don't allow * to start such a statement). This is the same mechanism used to distinguish object patterns from blocks.

If you do that, the ASI hazard goes away, no cover grammar needed.

The only other hazard is yield * () => x. This will need a similar negative lookahead for yield (but not yield*) to avoid ambiguity, letting that sequence continue to evaluate as yield* (() => x) for compatibility as it does today.

Other than that, I don't see anything else that could be in the way of the syntax *() => ....

cowboyd commented 6 months ago

@gurgunday I agree whole heartedly that it is important to focus, and my vote would be the same as @dead-claudia:

*() => ... FTW

I don't actually have a vote, but as someone who spends most days using JavaScript generators I do have an outsized interest.

So yes, winnowing the discussion to one of the options you've outlined would be a great first step. So how does this decision to winnow get made? Assume it would take some action from @chicoxyzzy?