Description
As far as I understand the current design of AsyncFn*
traits allows them to become somewhat of a “sugar” for a more generalized approach in the future. Quoting from the RFC:
Changing the underlying definition to use
LendingFn*
As mentioned above,
async Fn*()
trait bounds can be adjusted to desugar toLendingFn*
+FnOnce
trait bounds, using associated-type-bounds like:where F: async Fn() -> i32 // desugars to where F: for<'s> LendingFn<LendingOutput<'s>: Future<Output = i32>> + FnOnce<Output: Future<Output = i32>>This should be doable in a way that does not affect existing code, but remain blocked on improvements to higher-ranked trait bounds around GATs. Any changes along these lines remain implementation details unless we decide separately to stabilize more user-observable aspects of the
AsyncFn*
trait, which is not likely to happen soon.
In particular, the case for AsyncFnOnce
seems pretty clear. It’s a future possibility (which is IMO very desirable) that AsyncFnOnce(Args…) -> R
might be automatically implemented for any implementor of FnOnce(Args…) -> F
that returns some F: Future<Output = R>
; either by turning it into a sort of “alias” (in a way that avoids the shortcomings mentioned in the RFC) or by finding a way to re-structure the blanket impls.
In particular, IMO it’s a very relevant future possibility that Box<dyn FnOnce(Args…) -> Pin<Box<dyn Future<Output = R> [+ Send] [+Sync]>>
could become an implementor of AsyncFnOnce(Args…) -> R
in the future.
But #[fundamental]
kills this possibility, as the following code compiles successfully, powered by the negative reasoning that #[fundamental]
provides:
use std::pin::Pin;
use std::future::Future;
type BoxedFnOnceReturningBoxFuture = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()>>>>;
trait MyTraitNoOverlap {}
impl MyTraitNoOverlap for BoxedFnOnceReturningBoxFuture {}
impl<F: AsyncFnOnce()> MyTraitNoOverlap for F {}
I was unable to find any prior discussion about the value of why these traits (AsyncFn
, AsyncFnMut
, AsyncFnOnce
) are marked #[fundamental]
in the first place. I can imagine it’s perhaps by analogy to Fn
, FnMut
, FnOnce
being marked as such. But this decision is an important trade-off that should be considered.
As far as I can tell, there should be no issue in simply removing the #[fundamental]
markers from AsyncFn
, AsyncFnMut
, AsyncFnOnce
before they’re stabilized in 1.85
. Testing this locally, std
still compiles find, and UI tests pass.
It should always be backwards-compatible to add back the #[fundamental]
marker later.
@rustbot label T-compiler, T-libs-api, F-async_closure, C-discussion
Activity
compiler-errors commentedon Feb 8, 2025
Yeah there's no reason for them to need to be fundamental; it's not like we need it for coherence in core today. Probably just an oversight from when I initially mirrored the
Fn*
traits. I'll remove it.compiler-errors commentedon Feb 8, 2025
There's no reason to prioritize this issue tho.
@rustbot label -I-prioritize
AsyncFnOnce
,AsyncFnMut
,AsyncFn
non-#[fundamental]
#136724steffahn commentedon Feb 8, 2025
That’s alright if there’s no need for it. I thought a future-compatibility issue on beta is a bit like a regression on beta 🤷🏻
cuviper commentedon Feb 8, 2025
Is it? At least for types, adding
#[fundamental]
can definitely break downstream code, like when we discussed tuples:https://rust-lang.zulipchat.com/#narrow/channel/144729-t-types/topic/.5BCoherence.5D.20Fundamental.20tuples
... but I don't know if there's a similar risk for traits.
compiler-errors commentedon Feb 8, 2025
For traits it's backwards-compatible to add fundamental, since it means something completely different. For types, it means that we ignore the outermost "type constructor" for the orphan check (e.g. we allow you to impl non-local traits for
Box<MyLocalType>
) -- adding that would open up the type for more overlap from downstream crates. For traits, it means that we assume the trait will have no new implementations added for existing types -- adding it is essentially "sealing" the trait at that point.cuviper commentedon Feb 8, 2025
Does that reasoning hold for the type
dyn Trait
as well?(but I see
AsyncFn*
aren't dyn-compatible anyway)compiler-errors commentedon Feb 8, 2025
Only ADTs (struct/enum/union) can be fundamental (for types).
theemathas commentedon Feb 8, 2025
If it were possible for users to define their own types in stable rust that implements the
AsyncFn
trait, then it would be hypothetically technically possible that rust later changing theAsyncFn
trait to be fundamental could cause previously semver-compatible versions of user crates to become semver-incompatible. I don't know if this counts as a breaking change in rust.For example, consider the following scenario (assuming that users can implement
AsyncFn
themselves):AsyncFn
trait.foo
version1.0.0
is published, containing a type namedThing
that doesn't do much.foo
version1.0.1
is published. It adds animpl AsyncFn for Thing
. This is allowed becauseAsyncFn
is not fundamental at this point.AsyncFn
trait to be fundamental.bar
is published. It depends on cratefoo
version^1.0.0
, and was tested againstfoo
version1.0.0
. This cratebar
unintentionally depends on the fact thatThing
doesn't implementAsyncFn
. The compiler allows this becauseAsyncFn
is now fundamental.foo
version1.0.1
alongside cratebar
. Things break, and the user complains.I can't figure out a way for this scenario to happen with the current feature set of stable rust though...
steffahn commentedon Feb 8, 2025
Of course, the issues of meta-semver-breakage… those are always easy to miss.
I guess, it’s a good thing you can’t (stably) implement
AsyncFn*
traits?Rollup merge of rust-lang#136724 - steffahn:asyncfn-non-fundamental, …
Rollup merge of rust-lang#136724 - steffahn:asyncfn-non-fundamental, …
Rollup merge of rust-lang#136724 - steffahn:asyncfn-non-fundamental, …
Unrolled build for rust-lang#136724
steffahn commentedon Feb 14, 2025
backport landed today 🚀 #136980
Rollup merge of rust-lang#136724 - steffahn:asyncfn-non-fundamental, …