Description
The following two calls fail:
fn fut_closure<F: FnOnce() -> Fut, Fut : Future<Output = ()>>(f: F) {
async_closure(f); // error, `F` is not `AsyncFnOnce`.
}
fn async_closure<F: AsyncFnOnce()>(f: F) {
fut_closure(f); // error, `F` is not `FnOnce`.
}
even though future-outputting fn
s (e.g., fn … -> impl FutureOrSubtraitThereof
), async fn
s, and async closure literals (async [move] |…| {…}
) implement both flavors of the {Async,}FnOnce
traits.
Why is this a problem
Rust 1.85.0 is too recent to be an acceptable MSRV for a bunch of projects, so the F: AsyncFnOnce
version may not be usable in certain libraries.
This, in turn, entails that "middleware" functions either use the same matching bound as the library used (unless they perform eta-expansion of the closure to get back the double-impl that the compiler provides to async closures literals):
fn middleware1(f: impl AsyncFnOnce()) {
::lib::api(f); // Error
}
fn middleware2<Fut : Future<Output = ()>>(f: impl FnOnce() -> Fut) {
::lib::api(f); // OK, but now `api` can't switch to `AsyncFnOnce` without breaking us.
} // (and also, neither can we!)
And because they might do the former, it also means that in the future, once the MSRV gets to become high enough, the library author cannot replace the FnOnce
bound with AsyncFnOnce
without causing breakage1.
Suggested Solution
Caution
EDIT: this does not work because of the &Fn => FnOnce
blanket impl: if we also had the suggested AsyncFnOnce => FnOnce
blanket impl as well, then there would be an overlap for impl &Fn + AsyncFnOnce
types.
- Add
: FnOnce<Args, Output = Self::CallOnceFuture>
as a super-trait ofAsyncFnOnce
, - blanket-implement
AsyncFnOnce
for allimpl FnOnce<Args, Output : Future>
types, - adjust the compiler-code to implement
FnOnce
only sinceAsyncFnOnce
is now covered by the blanket impl.
This means that library authors can keep using FnOnce
for lower-than-1.85.0 MSRV support, while hoping to migrate to the cleaner and nicer AsyncFnOnce
eventually, e.g._, 1.88.0? 🤞), without requiring a major bump.
- unless there are some compiler-specific implementation-related complications with doing this? cc @compiler-errors 🙏
Aside: this will not be possible for the AsyncFn{,Mut}
traits
Since they're actually special in how they're actually Future
-outputting Lending
Fn{,Mut}
s rather than bare Future
-outputting Fn{,Mut}
s. But this fact should not affect FnOnce
.
@rustbot label: +A-async-closures
Footnotes
-
there is also the question of explicit generic params vs. turbofish, but using a helper definition like in https://docs.rs/async-fn-traits does allow one to avoid exposing a turbofishable
Fut
in Rust 1.85.0, so this is not a true/genuine issue (just something to be mindful of). ↩