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 yield
ing) 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 loop
ed 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
yield
the iterator output,return
()
for exhaustion, and panic if resumed afterreturn
, at least. Needing to handle trait coloring of un/pinned&mut self
is 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 fromself
and there aren't any additional issues with representing input-derived borrows beyond the existing difficulties with higher-rankedFn
closures. ↩︎