tc39/proposal-function.sent

Why not just make it impossible to send values to the first next call in the first place?

Opened this issue · 4 comments

This proposal in my mind seems to trying to work around an issue that shouldn't exist in the first place. Although there may be some small risk of backwards incompatibility, it seems like it would make more sense if gen.next(x) resulted in an error on newly started generators when x is not undefined. This is the way Python does things with its .send method: gen.send(x) produces TypeError: can't send non-None value to a just-started generator.

If producing an error is too much, browsers could simply output a warning to the console. I just don't think it makes sense to completely change the "correct" way to get values from .next calls by introducing new syntax when the value passed to the first .next call really shouldn't be meaningful anyway.

It’d likely be backwards incompatible to do that at this point.

hax commented

@benburrill

This is an interesting idea.

I am not sure whether this idea was considered in the early days of generators. The generators of ES was borrowed from Python, and I remember the early generators prototype in JavaScript 1.7 had two methods: send(value) and next(). You need to first call next(), or send(value) will throw "can't call send() on newborn generator" (inaccurate, but something like that). This was very close to your idea.

For some reasons I don't know, these two methods were unified to ES6 next(value). Because only send() throw, I am not sure whether current no throw behavior is intentional or just follow original next(). Consider this proposal was started at Feb 2015 by @allenwb who is the editor of ES6, I guess allen may already decided to leave the room for this proposal when design ES6.

Whether it was "correct" or not, as @ljharb comment, we can't change the behavior of next() now.

Anyway, I think you have the point that whether this proposal is "meaningful". I will collect and add some use cases.

Thank you.

I don't know anything about the process for making changes to ES, but although raising an exception would be backwards incompatible, I personally don't think that should completely disqualify it. I doubt all that many people are intentionally passing values to the first call to next, and there could be a long deprecation period where implementations could spit out a warning (which would mostly accomplish the goal of an error without the pain).

But in any case, my main concern with this proposal is that it removes the consistency of the way sending things to generators work. Although a bit strange to the uninitiated, it is completely consistent and sensible for the first next call never to take a value. If it is changed so that depending on how the generator is written, generators sometimes get the first value from the first next call and sometimes in the second, it could make the interface more confusing to the caller, and I think as a result one of these conventions would need to become the "wrong" way to write generators.

I will collect and add some use cases.

I'd be interested in seeing them, but the way I see it, if we get along fine without being able to send initial values to generators in Python, what advantage would it really bring to JavaScript?

hax commented

@benburrill

although raising an exception would be backwards incompatible, I personally don't think that should completely disqualify it.

I also really want a way to deprecate some bad things in ES (for example some legacy syntax and apis in annex B 😅) ! The real problem is ECMAScript is a little bit different than other programming languages, you can never have a long enough deprecation period because there are many unmaintained websites in the internet and we don't want to make them break.

Of coz the boundary line of what is "backwards incompatible" sometimes vague. For example, if the original behavior was throw Error, we could change it to not throw. Strictly speaking this is also a "breaking change", but it's very rare that some code logic rely on the throwing behavior.

I doubt all that many people are intentionally passing values to the first call to next

I agree with you! The problem is they may not intentionally at all and the change still catch them! So the real problem is how we could know "how many" (whether it's intentional or not) and evaluate the risk. Browser vendor could do some data analysis, but everything have cost, u need to convince the browser dev team that the cost is worth for the issue. Personally I hope tc39 could improve this part. But in this specific issue, assume we finally find this proposal was not very useful, the best choice may be just withdraw the proposal without change anything which always have zero risk.

my main concern with this proposal is that it removes the consistency of the way sending things to generators work. Although a bit strange to the uninitiated, it is completely consistent and sensible for the first next call never to take a value.

This part is controversial, it's hard to say which one is more consistent.

For example, if u look the api in more static type way, it's impossible to mark the type of next(v) precisely in almost all static type system. Of coz both python and js are dynamic type, though now we have TS.

depending on how the generator is written...

We already depend on it. There are three possible usage of generator:

  1. only use iterator interface, aka. always call next() without param and use yield x as declaration
  2. only use "sender" interface , aka. always call next(v) (after first next()) and use v = (yield); to receive the value (but u also need extra yield; in the beginning to skip the first next())
  3. use both

it could make the interface more confusing to the caller

To be honest, I feel mixing bidirectional communication using one interface (let {value:b} = g.next(a) and a = yield b) is the root cause of the confusion. If we could redo everything I would prefer two separate interfaces:

  • next(void) and yield value (yield as declaration and never return value)
  • send(value: T):void and function.sent (so has nothing relate to yield)

Unfortunately we do not have the time machine (both for python and js). The best we can do now is something like:

  • next(void) and yield value
  • next(value T):void and function.sent

We could introduce linter rules or type inferring rules to help programmers separate the usages:

  • disallow v = yield value
  • if there is no function.sent in the generator, infer the type of the generator method as next(void)
  • if there is function.sent and no yield in the generator, infer the type of generator method as next(value: T): {value: void, done: boolean}
  • if there are both function.sent and yield value which means we are doing bidirectional communication, then infer the type of generator method as next(value: T_NEXT): {value: T_YIELD, done: boolean}

I believe this is at least better than current next(value: T) follow by next(void) which very hard to be statically analysis if you really want use generator for bidirectional communication.

if we get along fine without being able to send initial values to generators in Python, what advantage would it really bring to JavaScript?

I always believe the real motivation should be the use cases. If there are solid use cases of "only use sender interface" or "use both", then this proposal could bring some advantage.

To be honest, there are not much well known use cases of them both in python and js. In some degree, this is a cycle question, we don't know whether this is because "sender" interface and bidirectional communication are not very useful at all, or they are useful but u don't see many leverage generator just because the weird next() then next(value) and the need of first yield to skip the first next().