trait Coroutine, enum CoroutineState
Originally (futures@0.1), the Future trait baked in fallibility, with Future::poll returning Result<Poll<Self::Item>, Self::Error>. As part of uplifting into std, this was removed, and Future::poll now returns Poll<Self::Output> instead, with fallible futures using Output = Result<Item, Error>.
Are coroutines similar (i.e. shouldn't bake CoroutineState into the trait signature) or different (e.g. since they don't have Poll to indicate completion like futures do) here?
A key observation is that from the outside, Coroutine::resume looks the like any other subroutine; the underlying semicoroutine[1] has been transformed by the compiler to fit the usual stack structured control flow. It's for this reason I think the desirable primitive shape could look more like a FnPin trait[2], but I'm not particularly interested in discussing any naming/syntax except as it relates to the signature of Coroutine::resume (FnPin::call_pin).
The primary argument for -> CoroutineState<Self::Yield, Self::Return>, as I understand it, is that Coroutine encodes that it is a logic error to resume after it produces CoroutineState::Returned(_) (like with Iterator or Future producing None or Poll::Ready(_), respectively). If resume just returns Self::Output, there's no way for generic consumers of Coroutine can't know when a coroutine is exhausted.
But... is this actually an issue? FnMut has this same "issue" but it doesn't seem to be much of an issue in practice; APIs which take impl FnMut specify how they expect the callback to behave and everything seems to work out fine there.
There's a straightforward option for many yield closures: a call after a return resumes from the top of the closure. I really like this model conceptually because it means using yield doesn't change what return does. However, yield closures which move from captures (i.e. would be FnOnce if not yielding) cannot do so and need to poison any future resumption to panic after a return, and resuming after an unwind is yet another can of worms / potential divergence from non-yielding closure semantics (and potentially involving the unclear domain of UnwindSafe).
Of course, a coroutine with Yield=Output, Return=! is isomorphic to (and implicitly becomes the case for a looped yield closure body), meaning this should be a purely API design question. For non-yielding closures, the difference of "(typically) can call again after return" is directly represented with FnMut versus FnOnce.
The simplest option is, of course, to keep the machinery of the coroutine transform internal to the compiler and never expose it. Generators are a more straightforward API surface (well...[3]) to expose than coroutines, and every other language I can think of is generally happy using generators and/or async, using shared mutability (e.g. channels) to inject any "resume arguments". As such, maybe Rust doesn't need to invent a stable-ready solution for borrow verified semicoroutines.
Terms:
- subroutine
- single-entry, single-exit
- semicoroutine
- multiple-entry, single-exit
- coroutine
- multiple-entry, multiple-exit Note: entry/exit is referring to the calling convention. Single-entry means you only enter a routine at a single point/label; single-exit means you only exit back to the caller by paired
call/ret. (TCO breaks subroutine convention but imperceptibly to the caller.) A structured call stack is so ingrained to modern thinking that people usually mean semicoroutine when they say coroutine. ↩︎e.g.
↩︎trait FnPin<Args: Tuple>: FnOnce<Args> { extern "rust-call" fn call_pin(self: Pin<&mut Self>, args: Args) -> Self::Output; } // is Unpin bound needed? cf. future::poll_fn impl<A: Tuple, F: FnMut<A>> FnPin<A> for F; impl<A: Tuple, F: FnPin<A>> FnMut for Pin<&mut F>;In that they they unambiguously should
yieldthe iterator output,return()for exhaustion, and panic if resumed afterreturn, at least. Needing to handle trait coloring of un/pinned&mut selfis a problem for generators but not for coroutines (Pin<&mut impl ?Unpin>accepts both cases). Lending (GATfn(&mut self) -> Self::Output<'_>) is a question for both, but again worse for generators than coroutines, since the coroutine trait could bake in that its output potentially borrows fromselfand there aren't any additional issues with representing input-derived borrows beyond the existing difficulties with higher-rankedFnclosures. ↩︎