Skip to content

AsyncFnOnce(…) and FnOnce<(…), Output : Future> are not equivalent #139173

Open
@danielhenrymantilla

Description

@danielhenrymantilla

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 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.

  1. Add : FnOnce<Args, Output = Self::CallOnceFuture> as a super-trait of AsyncFnOnce,
  2. blanket-implement AsyncFnOnce for all impl FnOnce<Args, Output : Future> types,
  3. adjust the compiler-code to implement FnOnce only since AsyncFnOnce 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 LendingFn{,Mut}s rather than bare Future-outputting Fn{,Mut}s. But this fact should not affect FnOnce.

@rustbot label: +A-async-closures

Footnotes

  1. 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).

Metadata

Metadata

Labels

A-async-closures`async || {}`C-discussionCategory: Discussion or questions that doesn't represent real issues.T-langRelevant to the language team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions