`AsyncFnOnce(…)` and `FnOnce<(…), Output : Future>` are not equivalent
@compiler-errors is already working on this.
Since Mar 31, 2025.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
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 fns (e.g., fn … -> impl FutureOrSubtraitThereof), async fns, 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 breakage[^turbofish].
[^turbofish]: 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).
Suggested Solution
[!CAUTION]
EDIT: this does not work because of the&Fn => FnOnceblanket impl: if we also had the suggestedAsyncFnOnce => FnOnceblanket impl as well, then there would be an overlap forimpl &Fn + AsyncFnOncetypes.
- Add
: FnOnce<Args, Output = Self::CallOnceFuture>as a super-trait ofAsyncFnOnce, - blanket-implement
AsyncFnOncefor allimpl FnOnce<Args, Output : Future>types, - adjust the compiler-code to implement
FnOnceonly sinceAsyncFnOnceis 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 LendingFn{,Mut}s rather than bare Future-outputting Fn{,Mut}s. But this fact should not affect FnOnce.
@rustbot label: +A-async-closures
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.