Hmm… it’s interesting that at least under one of these proposals, await!(foo) and yield foo are both operations that (a) accept a value, (b) suspend the current function, and (b) return a value upon resuming; yet they work completely differently. await! desugars to calling foo.poll() repeatedly until it returns a value, then returns that. yield's “return value”, on the other hand, is actually an argument to the resume function.
Comparisons to other languages:
In Python, things are somewhat similar. yield suspends a generator and, if the caller resumes it using generator.send(foo), ‘returns’ the argument; otherwise it ‘returns’ None. await in Python is just a glorified yield from, which delegates to a sub-generator; it yields whatever the sub-generator yields, and when the sub-generator returns (aka throws StopIteration), there can be a return value, which becomes the return of yield from. Notably, if the sub-generator yields and the caller send()s a response, it goes directly to the sub-generator. So yield/send() can be used as a two-way communications channel between the original caller and whatever is at the top of the stack, though this often goes unused.
In JavaScript, both await and yield create a new closure and do the argument-to-return thing:
async function f() {
let x = await getSomePromise();
foo(x);
}
is roughly equivalent to
function f() {
return getSomePromise().then((x) => foo(x));
}
while
function* f() {
let x = yield 42;
foo(x);
}
is roughly equivalent to
function f() {
let iter = {
next: () => {
// overwrite the next() method
iter.next = (x) => {
foo(x);
return {value: undefined, done: true};
};
return {value: 42, done: false};
}
};
return iter;
}
JavaScript’s approach would definitely be suboptimal for Rust because it requires boxing. Python’s approach could work without boxing, but for the send channel to really be useful (IMO), it would need to be able to accept different types at different suspend points. Assuming this should be strongly typed, it’s obviously incompatible with an &mut self resume method. It could theoretically work if generators accept self by value and return a new generator, but even ignoring the performance implications, that seems like it would make for very gnarly type signatures.
Even for generators that don’t delegate, a yield return value which can only be one type throughout the function seems pretty niche to me, unlikely to be useful except in very simple cases.
Since the syntax is tricky, why not just get rid of it? Drop support for passing arguments to the resume function. If someone really wants them, they can always emulate them by returning a mutable slot to the caller for them to fill in: in futures-rs lingo, this is a futures::sync::oneshot::Sender. If needed, a wrapper function can recover the originally desired type signature, and with unsafe code this can probably be made equally efficient to a native implementation (at least once immovable types are implemented). But most code probably doesn’t need this at all.
(By the way, JavaScript’s closure-based approach can also be emulated on top of generators, and the same can almost be said for an API very similar to JS promises: monads… However, doing this properly would require the ability to clone generators.)