Compiler expecting "_" instead of "Future"

I get a compilation error, and this code is a very simplified use case:

pub struct TestStruct {}
pub struct TestParams {}

pub async fn use_func<T, R>(call_func: fn(&mut TestStruct, T) -> R, params: T)
where
    R: Future<Output = String>,
{
    let mut one = TestStruct {};
    let _ = call_func(&mut one, params).await;
}

pub async fn test01(_: &mut TestStruct, _: TestParams) -> String {
    String::from("")
}

pub async fn test_call() {
    let params = TestParams {};
    let _ = use_func(test01, params).await;
}

The error:

expected fn pointer `for<'a> fn(&'a mut TestStruct, TestParams) -> _`
      found fn item `for<'a> fn(&'a mut TestStruct, TestParams) -> impl Future<Output = std::string::String> {test01}`

Why the compiler expects "_" instead of "Future<Output = String>" (defined by the R type)?
How can I make it compile successfully?

I think this might be related to variance of R.

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

[1]

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.


  1. The first "desugaring" is using "type alias impl 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. ↩︎

  2. It does capture generic input types, but you don't happen to have any. ↩︎

  3. It will be a breaking change to remove the alias, should a more direct solution become possible. ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.