Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions text/0000-unified_coroutines.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Summary
[summary]: #summary

Unify the `Generator` traits with the `Fn*` family of traits. Add a way to pass to pass new arguments upon each resuming of the generator.
Unify the `Generator` traits with the `Fn*` family of traits. Add a way to pass new arguments upon each resuming of the generator.

# Motivation
[motivation]: #motivation
Expand Down Expand Up @@ -49,23 +49,23 @@ Yielded("World")

This RFC proposes the ability of a generator to take arguments with a syntax used by closures.
```rust
let gen = |name : &'static str| {
let gen = |name: &'static str| {
yield "Hello";
yield name;
}
```

Then, we propose a way to pass the arguments to the generator in the form of a tuple.
```rust
println!("{}", gen.resume(("Not used"));
println!("{}", gen.resume(("World"));
println!("{}", gen.resume(("Not used")));
println!("{}", gen.resume(("World")));
```
Which would also result in:
```rust
Yielded("Hello")
Yielded("World")
```
Notice that the argument to first resume call was unused, and the generator yielded only only the value which was passed to the second resume. This behavior is radically different from the first example, in which the name variable from outer scope was captured by generator and yielded with the second `yield` statment;
Notice that the argument to first resume call was unused, and the generator yielded only the value which was passed to the second resume. This behavior is radically different from the first example, in which the name variable from outer scope was captured by generator and yielded with the second `yield` statment;

The behavior, in which a generator consumes a different value upon each resume is currently not possible without introducing some kind of side channel, like storing the expected value in thread local storage, which is what the current implementation of async-await does.

Expand All @@ -81,18 +81,20 @@ let gen = |name :&'static str| {
name = yield name;
}
```
We are unable to denote assignment to multiple values at the same time.
We are unable to denote assignment to multiple values at the same time, and would therefore have to revert to using a tuple and possibly some kind of
destructuring assignment.

2. Creating a new binding upon each yield
```rust
let gen = |name :&'static str| {

let (name, ) = yield "hello";
let (name, ) = yield name;
}
```
We are creating a new binding upon each yield point, and therefore are shadowing earlier bindings. This would mean that by default, the generator stores all of the arguments passed into it through the resumes. Another issue with these approaches is, that they require progmmer, to write additional code to get the default behavior. In other words: What happens when user does not perform the required assignment ? Simply said, this code is permitted, but nonsensical:
```rust
let gen = |name :&static str| {
let gen = |name: &static str| {
yield "hello";
let (name, ) = yield name;
}
Expand Down Expand Up @@ -123,7 +125,7 @@ let gen = | name: &'static str| {
Introduces a new concept of a parametrized statement, which is not used anywhere else in the language, and makes the default behavior store the passed argument inside the generator, making the easiest choice the wrong one on many cases.


The design we propose, in which the generator arguments are mentioned only at the start of the generator most closely resembles what is hapenning. And the user can't make a mistake by not assigning to the argument bindings from the yield statement. Only drawback of this approach is, the 'magic'. Since the value of the `name` is magically changed after each `yield`. But we pose that this is very similar to a closure being 'magically' transformed into a generator if it contains a `yield` statement and as such is acceptable amount of 'magic' behavior for this feature.
The design we propose, in which the generator arguments are mentioned only at the start of the generator most closely resembles what is hapenning. And the user can't make a mistake by not assigning to the argument bindings from the yield statement. Only drawback of this approach is, the 'magic'. Since the value of the `name` is magically changed after each `yield`. But we pose that this is very similar to a closure being 'magically' transformed into a generator if it contains a `yield` statement and as such is an acceptable amount of 'magic' behavior for this feature.

![magic](https://media2.giphy.com/media/12NUbkX6p4xOO4/giphy.gif)

Expand Down Expand Up @@ -281,10 +283,10 @@ and utlize generators as a trait alias for a `FnPin<Args, Output = GeneratorStat

1. Increased complexity of implementation of the Generator feature.

2. If we only implement necessary parts of this RFC, users will need to pass empty tuple into the `resume` function for most common case, which could be solved by introducing a a trivial trait
2. If we only implement necessary parts of this RFC, users will need to pass empty tuple into the `resume` function for most common case, which could be solved by introducing a trivial trait
```rust
trait NoArgGenerator : Generator<()> {
fn resume(self : Pun<&mut Self>) -> GeneratorState<Self::Yield, Self::Return> {
fn resume(self: Pun<&mut Self>) -> GeneratorState<Self::Yield, Self::Return> {
self.resume_with_args(())
}
}
Expand Down Expand Up @@ -320,21 +322,21 @@ or a thread local storage, which introduces runtime overhead and requires `std`.

- Python & Lua coroutines - They can be resumed with arguments, with yield expression returning these values [usage](https://www.tutorialspoint.com/lua/lua_coroutines.htm).

These are interesting, since they both adopt a syntax, in which the yield expression returns values passed to resume. We think that this approach is right one for dynamic languages like Python or lua but the wrong one for Rust. The reason is, these languages are dynamically typed, and allow passing of multiple values into the coroutine. The design proposed here is static, and allows passing only a single argument into the coroutine, a tuple. The argument tuple is treated the same way as in the `Fn*` family of traits.
These are interesting, since they both adopt a syntax, in which the yield expression returns values passed to resume. We think that this approach is the right one for dynamic languages like Python or lua but the wrong one for Rust. The reason is, these languages are dynamically typed, and allow passing of multiple values into the coroutine. The design proposed here is static, and allows passing only a single argument into the coroutine, a tuple. The argument tuple is treated the same way as in the `Fn*` family of traits.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Proposed syntax: Do we somehow require assignemnt from yield expression(As outlined by [different pre-rfc](https://internals.rust-lang.org/t/pre-rfc-generator-resume-args/10011)), or we do we specify arguments only at the start of the coroutine, and require
explanation of the different behavior in combination with the `yield` keyword explanation ?
explanation of the different behavior in combination with the `yield` keyword explanation?

- Do we unpack the coroutine arguments, unifying the behavior with closures, or do we force only a single argument and encourage the use of tuples ?
- Do we unpack the coroutine arguments, unifying the behavior with closures, or do we force only a single argument and encourage the use of tuples?

- Do we allow non `'static` coroutine arguments ? How would they interact with the lifetime of the generator, if the generator moved the values passed into `resume` into its local state ?
- Do we allow non `'static` coroutine arguments? How would they interact with the lifetime of the generator, if the generator moved the values passed into `resume` into its local state?

- Do we adopt the `FnGen` form of the generator trait and include it into the `Fn*` trait hierarchy making it first class citizen in the type system of closures ?
- Do we adopt the `FnGen` form of the generator trait and include it into the `Fn*` trait hierarchy making it first class citizen in the type system of closures?

- Do we introduce the `FnPin` trait into the `Fn*` hierarchy and make `FnGen/Generator` just an alias ?
- Do we introduce the `FnPin` trait into the `Fn*` hierarchy and make `FnGen/Generator` just an alias?

# Future possibilities
[future-possibilities]: #future-possibilities
Expand Down Expand Up @@ -380,7 +382,7 @@ However, the main goal of this RFC is to provide a basis for these decisions and
# Addendum: samples
[addendum-samples]: #addendum-samples

The Generator concept is transformed into state machine on the MIR level, which is contained inside single function. The current implementation is transformed to something like this:
The Generator concept is transformed into a state machine on the MIR level, which is contained inside a single function. The current implementation is transformed to something like this:

```rust
let captured_string = "Hello";
Expand Down Expand Up @@ -419,7 +421,7 @@ let mut generator = {
};
```

After implementing changes in this RFC, the generated code could be approximated by this:
After implementing the changes in this RFC, the generated code could be approximated by this:

```rust
let captured_string = "Hello"
Expand Down