The problem
The way async fn
works is like so:
pub async fn test01(_: &mut TestStruct, _: TestParams) -> String { ... }
// becomes
pub type _Test01Out<'_> = impl Future<Output = String>;
pub fn test01(_: &mut TestStruct, _: TestParams) -> _Test01Out<'_> {
async move { ... }
}
// is close to
pub fn test01(_: &mut TestStruct, _: TestParams) ->
'_ + impl Future<Output = String>
{
async move { ... }
}
Note how the output type (the future) is parameterized by the input lifetime. That means for every input lifetime, the output type is a distinct type. (Types that differ by lifetime, even if they only differ by lifetime, are distinct types.)
But if we look at use_func
, we have these bounds:
pub async fn use_func<T, R>(call_func: fn(&mut TestStruct, T) -> R, params: T)
where
R: Future<Output = String>,
Type parameters like R
must resolve to a single type, so this function only takes call_func
which always return the same type -- that means they can't differ based on the input lifetime. That's the source of the error.
Solution one
RPIT (return position impl Trait
) works differently than async fn
: it doesn't capture input lifetimes, so it acts like this.[2]
pub fn test01(_: &mut TestStruct, _: TestParams)
-> impl Future<Output = String>
// becomes (no input parameter!)
pub type _Test01Out = impl Future<Output = String>;
pub fn test01(_: &mut TestStruct, _: TestParams) -> _Test01Out
That's the most direct fix today. However, note that the plan is to make RPIT work like async fn
in the next edition. If this actually fixes your use case, you should comment in the tracking issue.
If TAIT stabilizes, you can write the desugaring yourself. AFAIK this is the expected workaround to the edition change.[3]
Solution two
You can use type erasure.
// Perhaps also `+ Send`, ...
pub fn test01(_: &mut TestStruct, _: TestParams)
-> Pin<Box<dyn Future<Output = String>>>
{
Box::pin(async move {
String::from("")
})
}
But this one is more applicable when you need to name the type, which comes up in traits. It doesn't really gain you anything over the last solution, for your playground. It wouldn't gain you anything over TAIT, since type aliases are also nameable.
See also the async_trait
crate.
(async fn
/RPIT in traits recently stabilized, but is still not really complete.)
Solution three
Another approach is to make use_func
more general so it can support functions that return lifetime-capturing futures. fn()
types and the Fn()
bounds force you to name the output; for this approach to work, you need your own trait that removes this requirement.
It's possible to make the output of the future an associated type on TestFn
instead of a parameter.
The first "desugaring" is using "
type
aliasimpl Trait
(TAIT)", which isn't stable yet. The second "desugaring" actually adds a bound to the future, which is different than capturing the lifetime via a lifetime parameter, so it's not actually the same thing. ↩︎It does capture generic input types, but you don't happen to have any. ↩︎
It will be a breaking change to remove the alias, should a more direct solution become possible. ↩︎