One of the design decisions of both @vadimcn’s RFC and my implementation is to move async/await concerns to libraries. This avoids having future and stream concepts be a part of the language. So it doesn’t really make sense to expose async/await without also exposing generators to users.
gen arg
allows you to refer to the argument for Generator::resume
. Let’s look at @alexcrichton’s examples above, but print the argument passed to the generator in the loop.
@vadimcn’s RFC uses closure arguments to pass the arguments of the first resumption. All later arguments are returned as tuples by yield
.
fn range(a, b) -> impl Generator {
|mut argument| {
for x in a..b {
println("{}", argument);
(argument,) = yield x;
}
}
}
In my implementation, the argument to the resume function is implicit. It does not appear anywhere unless you refer to it by gen arg
. It acts like a normal variable, except it is not live when the generator is suspended. yield
does not return a value here. Unlike @vadimcn’s RFC, the way to access the arguments is the same at any point in the body.
fn range(a, b) -> impl Generator {
for x in a..b {
println("{}", gen arg);
yield x;
}
}
@vadimcn’s RFC reuses closure types as generator types. This is also the main reason implementing it would result in less compiler changes. However I feel that doing this is not user friendly or very type safe. It would be easy to be confused between generators and closures, which has very separate use cases. It would also increase the chances of introducing bugs to closures, while my branch has separate code for generators. Anyway I feel like difference in implementation concerns aren’t significant here and we should focus on language concerns.
I think @vadimcn’s RFC makes common use cases like iterators and async/await unwieldy and would be against the new ergonomics initiative. People will use compiler plugins to make it ergonomic, and we’d need such a plugin for each use case of generators (futures-rs, iterators, microkernel servers, etc.). I do not think designing language features which users won’t directly use is a good idea.
Having an utility closure makes difference between these two proposals more clear.
@vadimcn’s RFC:
fn foo() -> impl FnMut() -> CoResult<(), Value> {
||
let bar = |arg| {
|| {
await!(task(arg + 1));
await!(task(arg + 2));
}
};
await!(bar(1));
await!(bar(2));
}
}
My implementation:
fn foo() -> impl Generator<Return=Value, Yield=()> {
let bar = |arg| {
await!(task(arg + 1));
await!(task(arg + 2));
};
await!(bar(1));
await!(bar(2));
}
I want to block landing my branch in rustc on whether we go with an approach like @vadimcn’s RFC or what I implemeted. This is mostly due to the fact that changing the implementation is easier out of tree. I don’t think we should block on syntax, as changes to syntax is trivial.
For allowing borrows across suspend points, immovable types provides an solution. Now this probably requires marking generators as immovable or movable upfront, so we require syntax for that. The reason for this is that I do not know how to find out of a generator borrows across suspend points during type checking. Non-lexical lifetimes will probably make this impossible until after MIR generation.
Now if you feel like bikeshedding syntax, what is desired is syntax for marking bodies of function and closures as generators. We also require to mark generators as either movable or immovable. Note that this only applies to the bodies of function and closure and does not affect their signature. The syntax should also be consistient and not differ for function and closures. So using say gn
instead of fn
only works for functions. It is also a bad idea for a variety of other reasons. I don’t think using trait bounds on impl Trait
is a good idea for this. impl Trait + ?Move
should be a guarantee to the caller that the type never implements Move
, it should still be legal to return any movable type (including movable generators) it would just be treated as an immovable one. These things should be up to the impl Trait
language feature, which while useful is orthogonal. Also such bounds aren’t yet possible for closures. They would be very verbose for closures and will hide the underlying generator type which isn’t necessary.
There seem to be consensus for using yield
for suspending generators, so we should use that.
My implementation allows you to refer to the argument to generators with gen arg
. The syntax for this is up for bikeshedding. You may also argue on the names of the Generator
trait, State
struct and everything contained inside them.
Syntax for @vadimcn’s RFC would be a bit different as there is no gen arg
and it only requires marking closures as movable or immovable generators.
@rozaliev Your code is almost how my branch is intended to be used. Instead of returning impl Generator
, you should define a future trait, implement that for all suitable generators and just return impl Future
instead. Instead of having a Handle
type you should use for<'c> Generator<&'c mut Core>>
instead. You can then have unique access to the core with gen arg
. This doesn’t actually work in my branch yet. You can achieve a similar user interface by storing the Core
in TLS in the mean time. This just avoid passing around handles in your code.
Known bugs in my implementation:
- Generators never implement any OIBITs
- Generators doesn’t contain the types for variables which are live across suspend points from a type system perspective. This can result in unsoundness when generators actually contain types with lifetimes.
- Borrowing local variables across suspend point result in unsoundness if the generator’s memory location changes after it has resumed once.
If you find any further bugs in my implementation (https://github.com/Zoxc/rust/tree/gen), feel free to file an issue here: https://github.com/Zoxc/rust/issues