Hi everyone,
I'm refering to this topic here. I tinkered around a little bit.
This example should illustrate a standard case where async_fn_traits
can help.
My understanding know is that you cannot tie an opaque return value like a Future to a HRTB lifetime with the Fn(T) -> U
syntax therefore you need something like the async_fn_traits
workaround (as already has been mentioned in the other topic).
But there's still one puzzle piece that I'm missing: When I use for example AsyncFn1
like in the outcommented part of the playground snippet above where exactly in the trait definition of AsyncFn1
is the part that ensures that the lifetime of the async function output (impl Future<Output=()> + '_
) is the same as the lifetime of the exclusive reference which gets passed to test_func
in the example? ... Might it be that passing &'a mut u32
as generic parameter for AsyncFn1
implies this?
Regards
keks
Let's give these names:
// TheFn TheFuture<'a>
// vvvvvvvvv vvvvvvvvvvvvvvvvvvvvvvvvvvv
fn test_func(x: &mut u32) -> impl Future<Output=()> + '_ {
async {
*x = 2;
}
}
// Compiler supplied implementations:
impl<'a> FnOnce<(&'a mut u32,)> for TheFn {
type Output = TheFuture<'a>;
/* ... */
}
impl<'a> FnMut<(&'a mut u32,)> for TheFn { /* ... */ }
impl<'a> Fn<(&'a mut u32,)> for TheFn { /* ... */ }
impl<'a> Future for TheFuture<'a> { type Output = (); /* ... */ }
Then as part of the blanket implementation:
impl<F: ?Sized, Fut, Arg0> AsyncFn1<Arg0> for F where
F: Fn(Arg0) -> Fut,
Fut: Future,
The types above are handled as-if there was an more specific implementation:
impl<'a> AsyncFn1<&'a mut u32> for TheFn {
type OutputFuture = TheFuture<'a>;
type Output = ();
}
Because that's what the signature of test_fn
matches.
The trait definition doesn't force a relationship between the input and the output, but it allows one. If the output has a non-'static
lifetime, it must have come from the input (Arg0
) -- there's nowhere else for it to come from in the implementation.
But the output could also be always 'static
, or capture some but not all non-'static
lifetimes from the input, etc.
The Fn
traits similarly allow but do not force a relationship between input and output (else we couldn't have non-borrowing functions). The key differences from the Fn
traits are
- You're not forced to name the associated output type in bounds
- E.g. the
R
in your playground, which must be a single type, not a type constructor with a lifetime parameter
- Having the bound you desire (
Future<...>
) on the associated type
- Maybe just straight-up more flexible with HRTB not forcing
'static
too, though that's not important for this example
2 Likes
Thanks so much! Now the matter is clear to me. 
Sorry, for bringing the topic back to the top, just one last question:
The formal reason that the code snippet doesn't compile is that fn types are contravariant covariant over their input output parameter, right?
It's because the return value needs to be a different type depending on the input lifetime/type.
Types that differ only by lifetime are still distinct types, and something like &str
(for any lifetime) isn't a type - it's a type constructor for every concrete &'a str
, &'b str
...
The variance doesn't matter; they're distinct types whether covariant, contravariant, or invariant.
The conflict is that R
must be a single type (and thus independent of the input lifetime/type). (And Rust doesn't have generic type constructor parameters, so there's no direct fix.)
Make sense?
1 Like
Yes!
Thanks again. 
Due to the compiler message:
...
= note: expected opaque type `impl for<'a> Future<Output = ()> + '_`
found opaque type `impl Future<Output = ()> + '_`
...
I thought you could argue with covariance as the return type of test_func ist more general than the return type of F in test_wrapper but as you explained this seems to have nothing to do with variance.