-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal for generators design #2873
Comments
I've updated the proposal with the results of further discussion. There have only been a few minor changes:
|
For the sake of completeness, I think it would extremely helpful to actually state the current declarations of the types named here: interface IteratorResult<T> {
done: boolean;
value?: T;
}
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}
interface GeneratorFunction extends Function {
}
interface GeneratorFunctionConstructor {
/**
* Creates a new Generator function.
* @param args A list of arguments the function accepts.
*/
new (...args: string[]): GeneratorFunction;
(...args: string[]): GeneratorFunction;
prototype: GeneratorFunction;
}
declare var GeneratorFunction: GeneratorFunctionConstructor;
interface Generator<T> extends IterableIterator<T> {
next(value?: any): IteratorResult<T>;
throw(exception: any): IteratorResult<T>;
return(value: T): IteratorResult<T>;
[Symbol.iterator](): Generator<T>;
[Symbol.toStringTag]: string;
} |
Looks good! |
Got a little lost in the first post, but I'm going to write what I understood, and you guys can correct me if I'm wrong: function *g () {
var result: TNext = yield <TYield>mything()
}
Is there anything important that I missed here? Request: function* g(value: number) {
while (true) {
value+= yield value;
}
} something like: var ginst: GeneratorInstance<number, number> and var gtype: *g(start: number)=>GeneratorInstance<number, number>; For the following code: ginst = g(0);
ginst.next(2);
gtype = g; 👍 for generators edit: fixed putting *'s in all the wrong places. |
... Also the lack of a return statement annoys me, I think it should be forced to have the same type as yield, and if it's a different type (and yield is being implicitly typed) the return type should force a change to the implicitly derived type for yield. This way I can have my generators actually end on a value that's not forced to be undefined (by Typescript). |
@Griffork from what I understand, you can have return statements, just not return expressions - specifically, you can't return a value, but you can bail out from within the generator at any point. This probably doesn't help your frustration in the return type being ignore; however, it would certainly help to get some use realistic cases for what exactly you'd like to return when a generator has terminated. |
@DanielRosenwasser not sure I understand. Here's an example of the type of generator I was thinking of when I voiced my discomfort: function* g (case) {
while(true){
switch(case) {
case "dowork1":
//do stuff
case = yield "OPERATIONAL - OK";
break;
case "dowork2":
//do stuff
case = yield "OPERATIONAL - OK";
break;
case "shutdown":
//do stuff
return "COMPLETE";
}
}
} Where it may execute an arbitrary amount of times, but at some point it's "completed" and it notify's it's caller that it's done. My concern (which I have not yet researched) is that without the return statement, there might be garbage-collection problems on some systems (particularly since the whole function-state has to be suspended and resumed on a yield), which is bad if you're spawning a lot of similarly-structured generators/iterators. It also makes the function read a lot more clearly in my opinion. |
That is a return statement, for which the return expression is In other words, a return expression is the expression being returned in a return statement.
From what I understand of your example, you return |
Though, now that I think about it, if there are multiple ways to terminate (i.e. shutdown or failure), that's when the returned value in a state-machine-style generator would be useful. |
@DanielRosenwasser got it, thanks for the clarification :). |
I'd argue that a correct implementation would allow return expressions, and type them distictly from yield expressions. Generators are commonly used in asynchronous task runners, such as co. Here is an example: var co = require('co');
var Promise = require('bluebird');
// Return a promise that resolves to `result` after `delay` milliseconds
function asyncOp(delay, result) {
return new Promise(function (resolve) {
setTimeout(function () { resolve(result); }, delay);
});
}
// Run a task asynchronously
co(function* () {
var a = yield asyncOp(500, 'A');
var ab = yield asyncOp(500, a + 'B');
var abc = yield asyncOp(500, ab + 'C');
return abc;
})
.then (console.log)
.catch (console.log); The above program prints The The In this use case, |
@yortus I'm not sure what you're asking for is at all possible, or if it makes any sense, I'll try to explain where I'm confused. The only way to start or resume a generator is the generator's The following Javascript: function*g() {
var a = yield "a";
var b = yield a + "b";
var c = yield b + "bc";
return 0;
}
var ginst = g();
console.log(g.next() + g.next("a") + g.next("a"));
return g.next(""); Is the equivalent to console.log(("a") + ("a" + "b") + ("a" + "bc"));
return 0; But what happens if I try: var done = false;
var value;
while (!done) {
value = ginst.next(value);
console.log(value);
} I get: "a"
"ab"
"abbc"
0 The last one is a number, meaning if It's important to note here that the proposal that yield and return are treated identically will work for co's consumption, and for Promises. If it will help I can write some example implementations. |
Like that last suggestion:
Seems ok to lose the correctness of next when you subsume the generator into an iterable/iterator. Does that solve
|
@jbondc |
If you read |
Another example of using generators to support asynchronous control flow. This is working code, runnable in current var co = require('co');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var path = require('path');
// bulkStat: (dirpath: string) => Promise<{ [filepath: string]: fs.Stats; }>
var bulkStat = co.wrap(function* (dirpath) {
// filenames: string[], TYield = Promise<string[]>
var filenames = yield fs.readdirAsync(dirpath);
var filepaths = filenames.map(function (filename) {
return path.join(dirpath, filename);
});
// stats: Array<fs.Stats>, TYield = Array<Promise<fs.Stats>>
var stats = yield filepaths.map(function (filepath) {
return fs.statAsync(filepath);
});
// result: { [filepath: string]: fs.Stats; }
var result = filepaths.reduce(function (result, filepath, i) {
result[filepath] = stats[i];
return result;
}, {});
// TReturn = { [filepath: string]: fs.Stats; }
return result;
});
bulkStat(__dirname)
.then(function (stats) {
console.log(`This file is ${stats[__filename].size} bytes long.`);
})
.catch(console.log);
// console output:
// This file is 1097 bytes long. The function Note that the |
@yortus Without dependent types, it becomes very hard to hold onto TReturn without having it pollute TYield. Ideally, we would have one type associated with @jbondc, I understand your syntactic concern with |
Replying in phone, bear with me... @JsonFreeman oh, good point. I stopped monitoring the straw man before for... of was finalised. The use case that I currently have for return is the state machine example above when you consider that you can also return "ERROR". Yes, I plan to do some funcy promise-like stuff with a next-able state based generator. Being able to detect type depending on the value of |
@JsonFreeman would it be possible to opt in/out of returning a value? I don't know where your facts about the typical usage of generators comes from, an article like that would be useful to read, would I be able to get a link? |
|
It would essentially involve hacking the assignability rules to make sure a generator that returns something is assignable to an iterable when you ignore that return value. Doable, but kind of a hack. |
Oh, ok. I see what you mean about the problems with making generators sometimes not iterable. If it's going to be a hack, either don't do it or don't do it yet, leave it to the user and if it's a big problem later you can revaluate the decision. As for opting in/out of returning a value, yes. When I first wrote that I was thinking of something else, but that idea was bad and this one is better. Again, I don't think you can separate the return type from the yield type due to the way generators are used (although I agree it would be useful, JavaScript's implementation does not make this doable). |
@Griffork here is an in-depth article describing many uses and details of generators. TL;DR: the two main uses cases so far are (1) implementing iterables and (2) blocking on asynchronous function calls. @JsonFreeman having Interestingly, when crafting generators to pass to Side note: the proposal for async functions (#1664) mentions using generators in the transform for representing async functions in ES6 targets. Return expressions are needed there, in fact the proposal shows one in its example code. It would be funny if |
@JsonFreeman #2936 mentions singleton types are getting the green light. At least for string literal types. If there was also a boolean literal type, then the I'm just thinking out loud here, so not sure if that would make anything easier, even it if did exist. |
@Griffork and @yortus, thank you for your points. It sounds like we are leaning towards the solution of the "next" parameter and the return values having type @yortus, as for singleton types, let's see how it goes for strings, and then we can evaluate it for booleans. At that point it would be clearer whether it would help split up TYield and TReturn, but I imagine that it could be just what we need here. |
@JsonFreeman sure, I'd be happy with that. Thank you for listening, this has been one of the most enjoyable discussions I've had on a Typescript issue :-). |
@JsonFreeman sounds good. Another minor point: interface Generator<T> extends IterableIterator<T> {
next(value?: any): IteratorResult<T>;
throw(exception: any): IteratorResult<T>;
return(value: T): IteratorResult<T>; // <--- value should not be constrained to T
[Symbol.iterator](): Generator<T>;
[Symbol.toStringTag]: string;
} That's copied from above. Shouldn't the |
Can you elaborate? Why would it get no typing exactly? Example maybe? |
Typescript would be not modelling that language, but modelling the "preferred use". And no one would be able to compete against the "preferred use". The draw to Typescript for me (over coffee script and others) is that they made no assumptions about how you're going to use the language, everything is 'fair game' (which is why I can modify Array.prototype). |
Sure: var (val1,val2) = yield await(asyncgen1), await(asyncgen2);
|
*square brackets, I couldn't remember the destructuring syntax |
@Griffork when you say 'your library', what library are you talking about? And what does 'preferred use' mean?
|
It could (with some very library/use specific and code, like the code you're suggesting that is library/use specific) 'my library' is a library that I'm working on (library in the same fashion co is a library). |
@Griffork I haven't proposed any preferred use, so I still don't follow your meaning here. Can you elaborate? The only code I mentioned that is library/use specific, like TypeScript lets you optionally type all kinds of other things, if you can provide a static description of them. That doesn't make all these typings somehow 'preferred'. They reside in the libraries where they belong. |
@yortus Typescript lets you type Javascript. It only (so far) supplies semantics for describing how raw javascript works, not for describing how libraries work. All of these libraries that do async that are common now (e.g. angular, co, etc.) currently all use promises, but that doesn't mean in the next year promises are going to be the most common way of doing async with generators, we don't know that yet. Why, if you're going to support using generators with promises, can't I also insist that the Typescript team support how I'm going to use generators (because yes, it is possible, just not very feasible and not useful for more than that one way of using generators). My initial points for async (which you quoted out of context) were in the context that async would be just as common (not more so) than iterators, and they should be supported equally. If you're going to argue support for a specific way of doing async, then I argue for support for any other way of doing async. |
After some discussion, here is the current plan:
As a result, I will add good support for generators as iterables for now. This is because it is possible to support that use case well now, whereas supporting the async use case should be built on top of boolean literals. Async use cases will still be possible, just not strongly typed. After boolean literals, we can better support async use cases. |
@JsonFreeman I'm curious, what was the reasoning behind ignoring return types vs using option 5 (special hack)? |
@JsonFreeman sounds like a good start. 'For now, yield expressions will be required to have a common supertype'. This means that for any of the async examples I've given in this thread to compile, they will have to be explicitly annotated with @Griffork TypeScript would know nothing about promises under my proposal. Not sure why you think that. I wholeheartedly agree with your point, but it just doesn't apply to the technique I proposed. |
The reasoning behind not doing the hack (option 5) is that we have a better long term solution that is not a hack. Doing the hack would give us some value in the short term and none in the long term. And while I think tracking the return type is important, I do not think it is urgent enough to warrant the hack that we will later remove. For the common type issue, yes you must provide Yield expressions themselves will have to be explicitly typed inline for now, yes. |
OK I can live with that for a version or two. Better |
@JsonFreeman yep, fair. |
I would love to have async/await compile to ES5 prioritized. Can you elaborate why generators/yield gets preference? |
@cveld because async/await is sugar around generators and promises. And, of course, generators is standard already and async/await not, hence subject to change (rare chances, but anyway). |
And when do you expect that ES5 compilation for generators/yield will 2015-09-07 18:17 GMT+02:00 Arthur Stolyar notifications@github.com:
|
@cveld there is no time line for this at the point. |
A generator is a syntactic way to declare a function that can yield. Yielding will give a value to the caller of the next() method of the generator, and will suspend execution at the yield point. A generator also supports
yield *
which means that it will delegate to another generator and yield the results that the inner generator yields.yield
andyield *
are also bi-directional. A value can flow in as well as out.Like an iterator, the thing returned by the next method has a done property and a value property. Yielding sets done to false, and returning sets done to true.
A generator is also iterable. You can iterate over the yielded values of the generator, using for-of, spread or array destructuring. However, only yielded values come out when you use a generator in this way. Returned values are never exposed. As a result, this proposal only considers the value type of next() when the done property is false, since those are the ones that will normally be observed.
Basic support for generators
Type annotation on a generator
A generator function can have a return type annotation, just like a function. The annotation represents the type of the generator returned by the function. Here is an example:
Here are the rules:
The type annotation must be assignable to.Iterable<any>
IterableIterator<any>
must be assignable to the type annotation instead.yield *
expression must be assignable toIterable<any>
yield *
expression must be assignable to the element type of the generator. (string is assignable to string)yield
(if present) expression is contextually typed by the element type of the generator (string)yield *
expression is contextually typed by the type of the generator (Iterable<string>
)yield
expression has type any.yield *
expression has type any.The generator is allowed to have return expressions as well, but they are ignored for the purposes of type checking the generator type.The generator cannot have return expressionsInferring the type of a generator
A generator function with no type annotation can have the type annotation inferred. So in the following case, the type will be inferred from the yield statements:
yield *
operands.yield *
expression must be assignable toIterable<any>
yield
andyield *
expressions again have type anyyield
expressions are contextually typed by the element type of the contextual typeyield *
expressions are contextually typed by the contextual type.Again, return expressions are allowed, but not used for inferring the element type.Return expressions are not allowed. Consider relaxing this later, particularly if there is no type annotation.yield *
expressions, what should the element type be?The
*
type constructorSince the Iterable type will be used a lot, it is a good opportunity to add a syntactic form for iterable types. We will use
T*
to meanIterable<T>
, much the same asT[]
isArray<T>
. It does not do anything special, it's just a shorthand. It will have the same grammatical precedence as[]
.Question: Should it be an error to use
*
type if you are compiling below ES6.The good things about this design is that it is super easy to create an iterable by declaring a generator function. And it is super easy to consume it like you would any other type of iterable.
Drawbacks of this basic design
This implies that maybe we should give an error when return expressions are not assignable to the element type. Though if we do, there is no way out.
2. The types of
yield
andyield *
expressions are just any. Many users will not care about these, but the type of theyield
expression is useful if for example, you are implementing await on top of yield.3. If you type your generator with the
*
type, it does not allow someone to call next directly on the generator. Instead they must cast the generator or get the iterator from the generator.To clarify, issue 3 is not an issue for for-of, spread, and destructuring. It is only an issue for direct calls to next. The good thing is that you can get around this by either leaving off the type annotation from the generator, or by typing it as an IterableIterator.
Advanced additions to proposal
To help alleviate issue 2, we can introduce a nominal Generator type (already in es6.d.ts today). It is an interface, but the compiler would have a special understanding of its type arguments. It would look something like this:
Notice that TReturn is not used in the type, but it will have special meaning if you are using something that is nominally a Generator. Use of the Generator type annotation is purely optional. The reason that we need to omit TReturn in the next method is so that Generator can be assignable to
IterableIterator<TYield>
. Note that this means issue 1 still remains.yield
expression will be the type of TNextGenerator<TYield, TReturn, any>
?Once we have TReturn in place, the following rules are added:
yield *
is a Generator, then theyield *
expression has the type TReturn (the second type argument of that generator)yield *
is a Generator, and theyield *
expression is inside a Generator, TNext of the outer generator must be assignable to TNext of the inner one.yield *
is not a Generator, and theyield *
is used as an expression, it will be an implicit any.Ok, now for issue 1, the incorrectness of next. There is no great way to do this. But one idea, courtesy of @CyrusNajmabadi, is to use TReturn in the body of the Generator interface, so that it looks like this:
As it is, Generator will not be assignable to
IterableIterator<TYield>
. To make it assignable, we would change assignability so that every time we assignGenerator<TYield, TReturn, TNext>
to something, assignability changes this toGenerator<TYield, any, TNext>
for the purposes of the assignment. This is very easy to do in the compiler.When we do this, we get the following result:
So you lose the correctness of next when you subsume the generator into an iterable/iterator. But you at least get general correctness when you are using it raw, as a generator.
Additionally, operators like for-of, spread, and destructuring would just get TYield, and would be unaffected by this addition, including if they are done on a Generator.
Thank you to everyone who helped come up with these ideas.
The text was updated successfully, but these errors were encountered: