-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Unified coroutines a.k.a. Generator resume arguments #2781
base: master
Are you sure you want to change the base?
Conversation
text/0000-unified_coroutines.md
Outdated
``` | ||
The example looks like we aren't assigning to the `name` binding, and therefore upon the second yield we should return the value which was passed into the first resume function, but the implementation of such behavior would be extremely complex and probably would not correspond to what user wanted to do in the first place. Another problem is that this design would require making `yield` an expression, which would remove the correspondence of `yield` statement with the `return` statement. | ||
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another downside of this approach is that a generator can't "naturally" hold on to passed-in values from previous invocations- they have to move them into other bindings if they want them to live across suspension points.
The let (name,) = yield name;
syntax is arguably more correct- you can shadow the argument name, but you can also name it something else, or even overwrite the existing binding. The key is that the yield
expression evaluates to the passed-in values, as it does in most coroutine implementations. That is, this should be valid:
let gen = |foo: T, bar: U| {
/* foo and bar are the arguments to the initial resume */
let (baz, qux) = yield ..;
/* foo and bar are *still* the arguments to the initial resume, but probably dead here */
/* baz and qux are the arguments to the second resume */
do_something(yield ..);
/* foo, bar, baz, and qux are still the arguments to the initial and second resume respectively */
/* do_something is passed a tuple of the arguments to the third resume */
};
The root of what makes this awkward is the fact that generators don't run any code until their first resumption, which means there is no yield
expression for the first set of passed-in values. This also potentially causes problems with lifetimes (see below), so it may be worth trying to come up with alternative non-closure syntax to declare the argument types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the pre-rfc I linked to tried to solve this by introducing different set of parameters for the Start
and Resume
, which would correspond to this syntactic choice.
Well, what is more correct behavior ? Take a generator, which has 10 yield points, each returing 3 values. In the approach in which the default behavior is to store passed arguments inside the generator, this generator has suddenly grown to contain 30 fields, even though user did not request this behavior. We might optimize them out, but conceptually, they are still stored inside the generator.
I believe not storing these values is more correct choice. This is the same choice, which is picked by FnMut
closures. But the issue of lifetimes is still present.
But this issue is mostly present here, on the source code presentation.Wouldn't dropping the arguments before yielding( in MIR representation) be a natural choice ?
On the applicable issues, I lean to the 'What's the behavior of closures ?' question to determine aproprate answer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already optimize out dead values in generators, that is not an issue. The problem is that your approach prevents the user from storing them even if they wanted to. (Without going out of their way to copy them somewhere else.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root of what makes this awkward is the fact that generators don't run any code until their first resumption, which means there is no yield expression for the first set of passed-in values.
it also means that you need to pass in the same type of value with which the generator is resumed in order to start it—this approach seems to make impossible, or at least syntactically complicated, starting the generator with arguments other than the resumption type, including starting it with no arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think forcing the user to move the values to other bindings might be a good thing. Saving a resume arg across yield points introduces a new runtime cost because that arg now has to be stored inside the generator object. (If it's not saved, it behaves like a function parameter to resume
and only lives for the duration of that call.)
That said, I strongly prefer having an explicit assignment at each yield
point, rather than an implicit one as this RFC proposes.
text/0000-unified_coroutines.md
Outdated
|
||
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe dynamic typing really changes things here. It's true that in dynamic languages you can come up with elaborate protocols where each resume takes a different set of argument types, but that's rare and confusing- most use cases stick with a single type, just as we are enforcing in Rust generators.
Letting yield
evaluate to the passed-in values, with the expression's type determined by the closure-syntax argument types, shouldn't cause any problems. Indeed, it simplifies things by introducing fewer new semantics (see above and below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it does not introduce new cognitive load but introduces fallible syntax, and makes the default/shorter code the wrong one in many cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have falliable syntax.
let x = {
let mut i = 2;
loop {
// May not necessarily add to i
i += if i > 16 {
i/2
} else {
// Does not return a value to increment i by,
// instead breaking out of the loop and causing it
// to evaluate to i
break i
};
}
};
text/0000-unified_coroutines.md
Outdated
|
||
- 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 ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do allow non-'static
coroutine arguments, some use cases will want arguments that live only for a single call to resume
. In this case, the syntactic approach proposed here doesn't work very well- the arguments would change lifetime (and thus type!) throughout the execution of the generator.
If instead the arguments are provided as the value of a yield
expression, each one could have a separate set of lifetimes limited by the actual live range of the expression's result. This fits much more naturally with how lifetimes work in non-generator code, especially with NLL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is the primary issue with my approach.
The generators in this case assume lifetime issues are not resolved at the source code / AST level, but rather at the control-flow / MIR level.
This means that in this case:
1. |a| {
2. loop {
3. println!("a : {:?}", a);
4. let (a,) = yield 1;
5. println!("b : {:?}", a);
6. let (a,) = yield 2;
7. }
8. }
The lifetime of a
both starts at the line 6 and ends at the line 4, and starts at line 1, and ends at line 4 depending on the entry point. But, these are 2 different values of a
, the behavior should be similar to the behavior which would be observed if we have written the generator by hand as a match statment, just like in the RFC.
fn take(a : &str, b : &str) {
match a {
"0" => {
take(b)
},
"0" => {
take(b)
}
}
}
In this case the b
has 2 exit points, and therefore its lifetime is extended to cover both, if i'm not mistaken.
But yes, the flow of lifetimes backwards is weird. But I think you can't escape it with re-entrant generators.
But the issue is still present with the generators that store the values by default . In these generators, the lifetime of the arguments would necessariliy have to include the lifetime of the generator as a whole, since they could be stored inside it ?
I was under the assumption that the borrowck, is MIR based, not AST based, and therefore this approach would be usable. Or is the checking of lifetimes performed on the AST level ? Need more info from compiler team.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a requirement for one of the primary usecases: futures. They will need to take Args = (&mut core::task::Context<'_>,)
which contains two lifetimes that are only valid during the current resume
call.
fixed small typos and formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were a few changes not related to the content of the RFC, but cleaned it up a bit
``` | ||
How the similar state machines are implemented today: | ||
```rust | ||
enum State { Empty, First, Second } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enum State { Empty, First, Second } | |
enum State { First, Second } |
You don't need Empty
to move out of sate in the for match
statement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is copied from existing state machines I found in the wild. I'd rather keep it that way.
enum Event { A, B } | ||
|
||
fn machine(state: &mut State, event: Event) -> &'static str { | ||
match (mem::replace(state, State::Empty), event) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to my last comment, you don't need to replace state
here with State::Empty
Another view of generators is that they are a more general form of |
Update 0000-unified_coroutines.md
Similar to |
I feel that the RFC as currently written spends a lot of “weirdness budget” in a way that is not necessary. In “normal” closures, the bindings introduced for arguments (between the two How about this alternative?
|
@SimonSapin You raise an important point. But, like closures, the arguments are available from the start of the generator, since generator is created Instead of a generator as it stands today, think about a pinned |
Hmm I may have been mislead by this part of the RFC:
I took this to mean that the proposed language semantics are that the argument to the first If we consider that this first value passed to So I think preference is closest to what is currently listed as alternative # 2. I’ll quote it below and comment inline:
I’d still phrase this as:
Yes about shadowing. But again, creating a binding doesn’t have to be mandatory.
Not necessarily, even with a binding. The value won’t be stored in the generator if it has been moved or dropped before the next
This is not nonsensical at all. Like any
|
In short, I feel strongly that Separately from the above, I feel there is a choice between:
|
Well, the argument HAS to be a tuple, and it HAS to be unpacked inside the argument list of the generator, since it is the behavior of closures, and deviating from this behavior would most certainly be a mistake. I again, point out the As for the third point, the argument would most certainly have to be a tuple. (Interaction of But yes, But there is something to be said about the default choice. Is it a good default to drop the values passed into resume ? I accept syntactic inconvenience, but I think, the deviation from concepts introduced in closures is a huge mistage. |
This would imply you need to scope every |
Yes, the goal of making generators and closures as close to each other as possible leads to arguments being a tuple. But I’m personally not very attached to that goal in the first place. Closures have a whole family of traits with @Nemo157 rust-lang/rust#57478 is an implementation bug to be fixed, right? |
Even if rust-lang/rust#57478 is fixed I would expect there to be a lot of generators that just shadow their existing bindings without moving out of them, having those all be kept alive to be dropped at the end would not be good, e.g. |arg: String| {
let arg = yield;
let arg = yield;
let arg = yield;
} would have to keep all 4 strings live until dropped when the generator completes. |
|
println!("{:?}", gen.resume(("Not used"))); | ||
println!("{:?}", gen.resume(("World"))); | ||
println!("{:?}", gen.resume(("Done"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(x)
is not a tuple. (x,)
is.
println!("{:?}", gen.resume(("Not used"))); | |
println!("{:?}", gen.resume(("World"))); | |
println!("{:?}", gen.resume(("Done"))); | |
println!("{:?}", gen.resume(("Not used",))); | |
println!("{:?}", gen.resume(("World",))); | |
println!("{:?}", gen.resume(("Done",))); |
We have also question: how does the |
I'd like to help the discussion along by summarizing the syntax discussion. I won't really address anything surrounding the implementation with trait definitions and whatnot. It seems the main issues of contention for syntax are the following: 0. Coroutine vs GeneratorFor some, both terms refer to the same syntactical and behavioral concept.. For others, coroutine refers to a generic behavior, and generator refers to the syntax allowing for easy definition of a specific kind of coroutine. The original RFC (2033) refers to them interchangeably. I will use the term "generators" to refer to the syntax and associated behavior that we are discussing, as that seems to be the least confusing. 1. Just a Closure with
|
Another thing to consider is the existing implementation, which works much like @SimonSapin has proposed: let mut g = |mut one_two: &str| -> &str {
println!("{}", one_two);
one_two = yield 0; // rebind a mut variable / argument
println!("{}", one_two);
let three = yield a + b; // bind to a new variable
three
};
println!("first resume: {:?}", Pin::new(&mut g).resume("one"));
println!("second resume: {:?}", Pin::new(&mut g).resume("two"));
println!("third resume: {:?}", Pin::new(&mut g).resume("three")); Given this syntax will work going forward (not necessarily true), that resolves the issues of defining types for resume args, binding resume args, and having separate types for yield and return. I think the idea of having only a single argument works well in this case because we can only pass a single value into or out of the generator at a time. It avoids issues of always needing to destructure a tuple and converting a list of arguments into a tuple. Remaining Issue: Do we provide a way to define the yield value type in the generator definition syntax? If so, how?I think we must provide a way to do so. Some options:
Also consider variations wrapping Any of these would solve the following two issues:
And could solve the following two as well if we make the
|
I've burned a lot of brain cells working on corountine craziness since last November. Both on my own and with @CAD97 & @pcpthm in a draft RFC. I'd like to say I have something concrete to show for it but, thanks to my own distractability and lack of time, it has mostly resulted in a few out-of-date rustc branches, a massive WIP blog post, etc. But I do think I have a good response to @pitaj's argument that "that the re-use of 'just' closure syntax and the behavior we want out of generators are impossible to reconcile". Those things can totally be reconciled! I can even explain such a proposal in two sentences:
To get "yield closures" in Rust you don't need any fancy I really really want to find time to evangelize this more. Talk about how it makes "magic mutation" kinda obvious and very sane, how it makes async streams practically trivial, how you might integrate The point is that any kind of new generator syntax (now available anywhere Rust is sold!) makes a lot of sense as a sugar. @pitaj's proposed fully qualified syntax could be implemented with something like: macro_rules! gyield {
($x:expr) => { {
yield GeneratorState::Yielded($x);
__coarg
} }
};
macro_rules! generator {
(|let $ap:pat: $at:ty = yield<$yt:ty>| -> $rt:ty $x:expr) => { |__coarg: $at| {
let $ap = __coarg;
yield GeneratorState::Completed($x);
loop { yield panic!("generator resumed after completion") }
} }
}; And there are cases where that sugar is quite useful! Having your different return/yield types automatically packed into an enum is pretty darn ergonomic. People want |
@samsartor I don't have any problem with the idea of closures and generators actually being the same thing. That sounds like a novel and interesting idea, and one that would reconcile them. What I was saying isn't reconcilable is having completely different behavior depending on whether a closure has yield or not. And it seems we agree on that point. However I don't see how you could have different yield and return types without also having a new syntax for specifying the yield output type. (Or you could just force everyone to use type inference for it, which I don't agree with). That's a minor issue and one easily solved. In fact, I'd say it makes sense that if the initial arguments into a generator are the same as the resume args, then it makes sense that the yield output type would be the same as the return type. I like your idea though. Essentially the idea is that any given closure is not necessarily complete when it returns a value. The result of calling it the next time is dependent on whether that return value was provided by a yield or by a return (implicit or explicit). Them you can reuse This could also prevent some overhead when used for async functions, since it can return the future variants directly without needing wrapped in a generator variant. It's more flexible, re-uses existing syntax, and is implemented as a fairly easy to understand extension to existing behavior. Edit: another thing to consider is what happens if the generator is exhausted. Does the next call restart at the beginning, or does it panic? I think it would be most consistent to restart at the beginning, since just calling any other type of closure won't panic AFAIK. |
I finally got round to revisiting my attempt at working out how to shim an argument accepting generator into a One thing I have realised is that the " async {
loop {
foo().await;
}
} The "single binding updated by |
I find it quite similar to Also, interaction with |
I have a bunch of ideas, but they involve more than generators feature.
For working with non-terminating errors there |
This is unnecessary, you can do that propagation in the loop body. Similarly with |
I came across this RFC today, and, after reading through the discussion, the thing I feel most strongly about is that simply introducing Option 1: Closure-like, creates regular
|
@rkjnsn Wow, this is a really great breakdown of these two options, and tracks pretty well with my understanding of the subject! The only correction I'd like to make is on "shadowing resume arguments" vs "reassigning resume arguments". Under MCP-49 (Option 1 above), the previous resume arguments are not shadowed. Instead they are completely dropped when entering a => |x| {
let y = &x;
yield;
dbg!(y, x);
}
error[E0506]: cannot assign `x` on yield because it is borrowed
--> src/lib.rs:3:4
|
2 | let y = &x;
| -- borrow of `x` occurs here
3 | yield;
| ^^^^^ assignment to borrowed `x` occurs here
4 | dbg!(y, x);
| - borrow later used here
|
= help: consider moving `x` into a new binding before borrowing
=> |x| {
let a = x;
let y = &a;
yield;
dbg!(y, x);
} As you mentioned, shadowing previous resume arguments can be quite confusing. But more importantly, it makes implementing poll functions needlessly difficult by introducing context reuse bugs in code like the following: std::future::from_fn(|ctx| {
if is_blocked() {
register_waker(ctx);
yield Pending;
}
while let Pending = task.poll(ctx) { .. }
}) In my experience, it is fairly rare that corountine authors want to reuse past arguments. And when they do, creating a new binding is a clear and obvious workaround. Hence, I'm pretty solidly in the reassignment camp. Can you yield from an async closure?In my mind, no. @CAD97, @pitaj, @pcpthm, and myself had quite a vigorous debate around this when drafting MCP-49 and basically came to the conclusion that there was no obvious way to accomplish it, at least for the time being. I think the reasoning is simple: coroutines are all about giving the user control over per-resume input but How do you use it to implement a Stream?Even without much syntactic sugar, it isn't too hard under MCP-49. A std::stream::from_fn(|ctx| {
while let Some(item) = await_with!(inner.next(), ctx) {
yield Ready(Some(await_with!(func(item), ctx)));
}
Ready(None)
}) Although we can all agree that a generator sugar makes this look nicer! async gen {
while let Some(item) = inner.next().await {
yield func(item).await;
}
} So generators or yield closures?Por que no los dos? This is just my opinion, but I think these are really two completely different features with two different goals. Trying to pack both into one syntax just creates a mess that is worse for both cases. Generators want only one thing: easily implement If you want to hear my thoughts on generalized coroutines, how they might support generators (as either a language feature or proc-macro crate), and what else they might have to offer (yield closures + iterator combinators = unlimited power), check out the design notes I wrote for the language team. |
To clarify my point on shadowing, it was indeed my understanding with option 1 that the existing arguments would dropped on yield, with the parameters getting set to the new arguments on resume. Indeed, I think that's one of the key benefits of this approach over some other suggestions for generalized coroutines discussed in this thread. My point about shadowing was that if user explicitly shadowed a parameter (the I was merely pointing out that this could seem a little weird at first ("how is the parameter reassigned when it isn't even visible?"), but that (to me, at least) it made sense after thinking about it for a bit. So, basically I was agreeing with you. 🙂
This seems reasonable. While I think you could probably get it to do something (basically, you'd have poll and resume as separate operations, and you'd have to poll to ready before you could resume), it wouldn't exactly be nice (it's basically two separate, commingled coroutines with a weird interface at that point), and the
Agreed. I was evaluating both options from a perspective of "assuming we want generalized coroutines, which syntax makes sense?" Given that resume arguments are where |
Ah, I totally misread your code example. It's actually kind of an interesting case I hadn't thought about, thanks for sharing! ThankfuIy, I think it is behavior that's kind of hard to get in the first place, and that users familiar with Rust's shadowing rules shouldn't be too surprised by it. It sounds like we're pretty much on the same page! 😁 |
I disagree with this, I would really like generator based |
The first option is more general. It helps writing any state machine, including the same as produced by In that light, I would:
|
We all want a nicer syntax for those traits! But I would say you are looking for a generalized coroutine syntax. For example, here is a sketch of using MCP-49's yield closures to decode a base64 stream: let mut decoder = |sextet: u8, octets: &mut ReadBuf| {
let a = sextet; // witness a, b, and c sextets for later use
yield;
let b = sextet;
octets.append_one(a << 2 | b >> 4); // aaaaaabb
yield;
let c = sextet;
octets.append_one((b & 0b1111) << 4 | c >> 2); // bbbbcccc
yield;
octets.append_one((c & 0b11) << 6 | sextet) // ccdddddd
};
io::read_from_fn(move |ctx, octet_buffer| {
// do some prep work during the first call to poll_read
pin!(inner);
let mut sextet_buffer = ReadBuf::new([MaybeUninit::uninit(); 1024]);
'read loop {
// wait for the inner reader to provide some bytes
await_with!(AsyncRead::poll_read, &mut inner, ctx, &mut sextet_buffer)?;
for byte in sextet_buffer.filled() {
while octet_buffer.remaining() == 0 {
// the given buffer is filled, the poll_read function should
// return Ready
yield Ready(Ok(()));
// pick up where we left off
}
// pass a byte of input to our decoder
decoder(match byte {
b'A'..=b'Z' => byte - b'A' + 0,
b'a'..=b'z' => byte - b'a' + 26,
b'0'..=b'9' => byte - b'0' + 52,
b'+' | b'-' => 62,
b'/' | b',' | b'_' => 63,
b'=' => return Ready(Ok(())),
e => yield Ready(Err(InvalidChar(e).into())),
}, octet_buffer);
}
}
}) Unlike yield closure implementations of |
And here's an example using async_io_macros::async_read! {
futures::pin_mut!(input);
loop {
let mut bytes = [0; 4];
let len = input.read(&mut bytes).await?;
if len == 0 {
break;
}
input.read_exact(&mut bytes[len..]).await?;
for byte in &mut bytes {
*byte = match *byte {
b'A'..=b'Z' => *byte - b'A',
b'a'..=b'z' => *byte - b'a' + 26,
b'0'..=b'9' => *byte - b'0' + 52,
b'+' | b'-' => 62,
b'/' | b',' | b'_' => 63,
b'=' => b'=',
_ => return Err(std::io::Error::new(std::io::ErrorKind::Other, "invalid char")),
}
}
let out = [
bytes[0] << 2 | bytes[1] >> 4,
bytes[1] << 4 | bytes[2] >> 2,
bytes[2] << 6 | bytes[3],
];
let mut out = if bytes[2] == b'=' { &out[..1] } else if bytes[3] == b'=' { &out[..2] } else { &out[..] };
while !out.is_empty() {
yield |buffer| {
let len = buffer.len().min(out.len());
buffer[..len].copy_from_slice(&out[..len]);
let (_, tail) = out.split_at(len);
out = tail;
Ok(len)
};
}
}
Ok(())
} I'm not 100% happy with the looped yielded closure for output, but I haven't had time to try and come up with a better syntax. Ah, I think I see the disconnect here, when you say generator you're specifically talking about something used for iterator/stream-like usecases? While I see generator (in Rust land) as being generalized coroutine syntax, since that's what it is currently on nightly. |
Yep. The generalized coroutine design notes go over this in detail, but the term "generator" is a bit overloaded in the Rust language design space right now. I tend to use it more to describe the generators in RFC-2996 (gen fn) than the ones in RFC-2033 (the current Generator trait). Neither is strictly wrong, but since I was making a point about including both yield closures (which are fairly similar to generators today) and gen functions (which are more like the async-stream or propane macros), the distinction was important. |
What if we make This makes things explicit about where the next resume argument will be stored, so that the magic mutation is no longer too confusing. UI with borrows is likely to be improved, as if there is a borrow of some binding, then users cannot assign to it, neither with plain assign nor by yielding to it (hence the requirement for receiver to be a mutable binding). And if desired, we could easily allow |
I don’t see what |
Another idea about the topic is that we can simply say that the resume argument isn't initialized before the first yield, that behavior would be surprising yet correct. |
After a lot of time and reading design notes many times, I got feeling that there are two entirely distinct level of abstractions involved:
I think that instead of trying to fit one feature on two uses, we have to design distinct UIs for the feature: The high-level case I imagine is more of that in RFC 2996, here we allow functions to have only yield and produce The low-level case and API is described in MCP-49. On behavioural part we don't do implicit restarting and instead always poison coroutine when final state is reached (we mention this in the docs, and ask users to explicitly We need to somehow distingush all these kinds, I think that more common (and one having less types involved) high level use deserves its own top-level syntax as of Traits are following:
Today, only Edit: And given that we can have two UIs, we can pick both sides on first resume problem:
|
i wonder: why not just do what async/await did and return the state machine after calling the generator? consider the following: let g = |stuff| {
yield stuff;
/* ... */
}; currently, this Usage example let mut a = g(5);
let mut b = g(9);
println!("{:?}", a.resume());
println!("{:?}", b.resume()); |
I think this is backwards. There is no need for different yield and return types or poisoning to be built in to the lower level feature, which should do nothing but add let mut store = Some(consume_once);
let mut coroutine = move || {
let consume_once = store.take().expect("poisoned!");
yield Yielded(42);
drop(consume_once);
Complete("hello")
};
assert_eq!(coroutine.resume(()), Yielded(42));
assert_eq!(coroutine.resume(()), Complete("hello"));
coroutine.resume(()); // poisoned! |
//Edit after a long time: For everyone, I suggest you read rust-lang/lang-team#49, it summarizes state of things, and explains ideas explored in this RFC and subsequent discussion.
This RFC outlines a way to unify existing implementation of the Generator feature with the
Fn*
family of traits and closures in general. Integrating these 2 concepts allows us to simplify the language, making the generators 'just pinned closures', and allows implementation of new patterns due to addtional functionality of generators accepting resume arguments.Generator resume arguments are a sought after feature, since in their absence the implementation of async-await was forced to utilize thread-local storage, making it
no_std
.Rendered
The RFC builds upon original coroutines eRFC
Main contention points:
Examples of new patterns designed with proposed design of the generator trait & Feature:
https://play.rust-lang.org/?version=beta&mode=debug&edition=2018&gist=c1750a50fbeff78200537d6f133ecb6e