-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Tracking issue for RFC 2033: Experimentally add coroutines to Rust #43122
Comments
cc #43076, an initial implementation |
Copied from #43076: I'm using this branch for stream-heavy data processing. By streams I mean iterators with blocking FS calls. Because Python: def my_iter(iter):
for value in iter:
yield value Rust with generators: fn my_iter<A, I: Iterator<Item=A>>(iter: I) -> impl Iterator<Item=A> {
gen_to_iter(move || {
for value in iter {
yield value;
}
})
} Two extra steps: inner closure + wrapper, and, worse, you have to write the wrapper yourself. We should be able to do better. TL:DR: There should be a built-in solution for |
I was a bit surprised that, during the RFC discussion, links to the C++ world seemed to reference documents dating back from 2015. There have been some progress since then. The latest draft TS for coroutines in C++ is n4680. I guess the content of that draft TS will be discussed again when the complete RFC for Rust's coroutines is worded, so here are some of the salient points. First, it envisions coroutines in a way similar to what this experimental RFC proposes, that is, they are stackless state machines. A function is a coroutine if and only if its body contains the The object passed to Various customization mechanisms are also provided. They tell how to construct the object received by the caller, how to allocate the local variables of the state machine, what to do at the start of the coroutine (e.g. immediately suspend), what to do at the end, what do to in case of an unhandled exception, what to do with the value passed to |
One subtle point that came up is how we handle the partially-empty boxes created inside of For example, if we have something like: fn foo(...) -> Foo<...> {}
fn bar(...) -> Bar<...> {}
box (foo(...), yield, bar(...)) Then at the yield point, the generator obviously contains a live |
I might be missing something in the RFC, but based on the definition of Here's an example of implementing the async/await pattern using coroutines in ES6. The generator yields Rust has a problem here because what's the type of TL;DR:
(Here are some more very interesting ideas for how to use two-way coroutines.) |
I don't know what you mean by "OIBIT". But at the yield point, you do not have a |
Looking at the API, it doesn't seem very ergonomic/idiomatic that you have to check if
Note that this would technically require adding an additional state to closure-based generators which holds the return value, instead of immediately returning it. This would make futures and iterators more ergonomic, though. I also think that explicitly clarifying that dropping a |
Has there been any progress regarding the |
https://internals.rust-lang.org/t/pre-rfc-generator-integration-with-for-loops/6625 |
I was looking at the current
Instead of relying on the programmer to not resume after completion, I would strongly prefer if this was ensured by the compiler. This is easily possible by using slightly different types: pub enum GeneratorState<S, Y, R> {
Yielded(S, Y),
Complete(R),
}
pub trait Generator where Self: std::marker::Sized {
type Yield;
type Return;
fn resume(self) -> GeneratorState<Self, Self::Yield, Self::Return>;
} (see this rust playground for a small usage example) The current API documentation also states:
So you might not immediately notice a resume-after-completion at runtime even when it actually occurs. A panic on resume-after-completion needs additional checks to be performed by In fact, the same idea was already brought up in a different context, however, the focus of this discussion was not on type safety. I assume there are good reasons for the current API. Nevertheless I think it is worth (re)considering the above idea to prevent resume-after-completion. This protects the programmer from a class of mistakes similar to use-after-free, which is already successfully prevented by rust. |
I too would have preferred a similar construction for the compile time safety. Unfortunately, that construction doesn't work with immovable generators, once they have been resumed they can't ever be passed by value. I can't think of a way to encode that constraint in a similar way for pinned references, it seems you need some kind of affine reference that you can pass in and recieve back in the |
A |
Note that |
Question regarding the current experimental implementation: Can the yield and return types of generators (move-like syntax) be annotated? I would like to do the following: use std::hash::Hash;
// Somehow add annotations so that `generator` implements
// `Generator<Yield = Box<Hash>, Return = ()>`.
// As of now, `Box<i32>` gets deduced for the Yield type.
let mut generator = || {
yield Box::new(123i32);
yield Box::new("hello");
}; |
I was hopeful that fn foo() -> impl Generator<Yield = Box<Debug + 'static>> {
|| {
yield Box::new(123i32);
yield Box::new("hello");
}
} it seems the associated types of the return value aren't used to infer the types for the (Note that One terrible way to do this is to place an unreachable yield at the start of the generator declaring its yield and return types, e.g.: let mut generator = || {
if false { yield { return () } as Box<Debug> };
yield Box::new(123i32);
yield Box::new("hello");
}; EDIT: The more I look at |
Yeah, I was hoping as well impl Trait would do the trick, but couldn't get it to work either. Your I guess the only way is to introduce more syntax to annotate the types? |
Will the |
I assumed that it would be changed, and I happened to be looking at the Pin RFC just now and noticed that it agrees, but it is blocked on object safety of arbitrary self types (which is currently an open RFC):
The third point has actually happened already, but it doesn't help much since that required making |
@NobodyXu ah yes that makes perfect sense of course. I suppose the hope was that the Rust internals had some magic type wrangling to hide what my mind wants to think of as "forwarding null". I see that would not be the case. Per resetting, that does make sense as well. Does that then imply that, when pure, @zesterer 's UnsafeCoroutine could be both trivial and safe? Per upstream handling of safety, very nice to hear that. |
I think adding a new unsafe method is definitely doable and achievable, though ideally compiler should optimize out the panic landing code. |
The reason for There are tricks to deal with the unsafety though. The code is generated by the compiler, so the implementation doesn't need @NobodyXu the compiler can't optimize it across dynamic dispatch but a special trait can. I'm not suggesting that the cost of dynamic dispatch is small enough for this to matter, just stating the fact. |
I wish
That's true, adding an Or maybe using a |
@NobodyXu note that I've since realized that Also I've found a way to return a zero-sized token instead of
The performance cost is the same - it's one branch and one byte in memory. Less panics probably means smaller binary but hardly anyone cares about it. |
…joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
…joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
Rollup merge of rust-lang#129034 - henryksloan:coroutine-must-use, r=joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
This comment has been minimized.
This comment has been minimized.
Hi @jonatino, Your example program seemingly does not exhibit undefined behavior as written, since the implementation of the one instance of the coroutine carefully re-assigns I do think coroutines can be useful in game development, in similar contexts but with a different implementation. That said, I would suggest that you learn the full capabilities of the language as it exists today, as there are other mechanisms that you can use to accomplish the same goal in a different way without requiring the stabilization of any features. |
will Coroutine trait support GAT? currently let iter = #[coroutine]
static || {
let v = vec![1, 2, 3];
for i in &v {
yield i;
}
}; is not work because
|
Consider supporting yield-once coroutines, which allow you to expose real references to ephemeral “parts,” like slices of arrays, not to mention completely synthetic parts that reference no persistent memory. Getting rid of proxy references would be a huge win for generic programming. |
@dabrahams Could you give a more concrete example (e.g. pseudo-rust code)?, because I don't think Rust has an equivalent to Swift' Accessors, and I can't imagine a scenario where that might be useful (except perhaps as a "strict" subclass of futures)... |
@fogti IIUC that change, if you have a way to ensure the generator is resumed after yielding, would give Rust the equivalent of Swift accessors, the lack of which creates numerous ergonomic problems. I'll try to come up with an example using your feature. Edit: having looked more closely, I think the closure literal syntax may not be compatible with this use case. My Rust-fu is too weak to write even passable Rust pseudocode for you, unfortunately. I'll ask someone I'm working with to see if they can help. |
@dabrahams can you confirm if this example works for the context: Consider this (pseudo-)rust code where proxy references might be necessary without yield once coroutine: pub struct MappedArray<In, Out, F, FInv>
where
F: Fn(&In) -> Out,
FInv: Fn(Out) -> In,
{
pub data: Vec<In>,
pub f: F,
pub f_inv: FInv,
}
impl<In, Out, F, FInv> MappedArray<In, Out, F, FInv>
where
F: Fn(&In) -> Out,
FInv: Fn(Out) -> In,
{
pub fn at_mut(&mut self, i: usize) -> yield_once &mut Out {
let mut mapped_val = (self.f)(&self.data[i]);
yield (&mut mapped_val); // assuming mapped_val would be mutated to desired value by caller
self.data[i] = (self.f_inv)(mapped_val);
}
pub fn push(&mut self, e: Out) {
self.data.push((self.f_inv)(e));
}
}
fn main() {
let mut arr = MappedArray {
data: Vec::default(),
f: |x| x + 1,
f_inv: |x| x - 1,
};
arr.push(2);
arr.at_mut(0) = 3;
} In this example, the correctness totally depends on the fact that control must come back to coroutine (most probably after last use of yielded value). For providing similar at_mut method for this class without yield once coroutine, proxy references would be necessary. A very similar usecase can be yielding a slice of array. With proposed syntax, I don't think we can yield a reference to support the above usecase, also I tried similar code and realized, it is not mandatory that control returns to coroutine after yielding at all. |
for "control needs to return to coroutine after yielding" I think using callbacks/closures (e.g. as parameter to |
@fogti, while passing closures works in this context, it does impact the ergonomics of the API. It introduces unnecessary nesting on the calling side, which can be cumbersome. With coroutine syntax, callers can still think sequentially, making the code more readable and maintainable. Furthermore, yielding references with coroutines will not lead to lifetime problems, as it is guaranteed that control returns to the coroutine, similar to the behavior when using closures. So, complex borrow checking might not be necessary. If I am correct, improving ergonomics was one of the main reasons why modify and read accessors were introduced in Swift, as constantly passing closures can be cumbersome. @dabrahams can provide more insights on this. |
@RishabhRD you might be onto something. If the call to "return reference" gets injected by the borrow checker after last use it could be a "reliable destructor". The problem is if you do it in a You're absolutely right that closure ergonomics is not great but in many cases it can be solved with a ref wrapper that has |
@RishabhRD looks good to me, with my limited Rust knowledge. For ensuring that control returns to a (non-yield-once) coroutine, you would still have to use a proxy, but maybe you could embed resumption into the destructor of the proxy. |
One thing to keep in mind about the proxy, and I think this applies to @Kixunil's ref wrapper too: if you rely on |
Actually |
Right, if we could also forbid holding the yielded reference across |
…ine, r=scottmcm re-export `FromCoroutine` from `core::iter` tracking issue: rust-lang#43122 fixes: rust-lang#135686
Rollup merge of rust-lang#135687 - joseluis:feat-reexport_from_coroutine, r=scottmcm re-export `FromCoroutine` from `core::iter` tracking issue: rust-lang#43122 fixes: rust-lang#135686
…ottmcm re-export `FromCoroutine` from `core::iter` tracking issue: rust-lang/rust#43122 fixes: #135686
RFC.
This is an experimental RFC, which means that we have enough confidence in the overall direction that we're willing to land an early implementation to gain experience. However, a complete RFC will be required before any stabilization.
This issue tracks the initial implementation.
related issues
The text was updated successfully, but these errors were encountered: