Extracting return type from async function

Given a consistent async function signature, I would like to create another type that is able to implicitly extract the complier-generated Future return type. No heap or dynamic dispatch.
(playground here).

use std::marker::PhantomData;

pub trait Trait {
}

pub struct Struct {}
impl Struct {
    pub async fn f(&self, i: u32) -> u32 {
        i
    }
}
impl Trait for Struct {
}

pub struct RetExtractor<F: Fn(&B, u32) -> Ret, Ret, B> {
    f: F,
    _p: PhantomData<(Ret, B)>
}
impl<F: Fn(&B, u32) -> Ret, Ret, B> RetExtractor<F, Ret, B> {
    pub fn new(f: F) -> Self {
        Self {
            f,
            _p: PhantomData,
        }
    }
}

fn stuff() {
    let s = Struct {};
    let e = RetExtractor::new(Struct::f);
}

You will get a peculiar error that says it expects X but is actually getting a matching X:

error[E0308]: mismatched types
  --> src/lib.rs:31:13
   |
31 |     let e = RetExtractor::<_, _, Struct>::new(Struct::f);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected associated type `<for<'_> fn(&Struct, u32) -> impl Future {Struct::f} as FnOnce<(&Struct, u32)>>::Output`
              found associated type `<for<'_> fn(&Struct, u32) -> impl Future {Struct::f} as FnOnce<(&Struct, u32)>>::Output`

If I remove the async keyword, this compiles. If I remove the self parameter from the F signature, it compiles.
If it had something to do with a binding to a specific lifetime, it seems removing the async keyword would still break since &self is still borrowed.
Note, on nightly, the errors are slightly different. Instead of impl Future, you see impl for<'_> Future. Perhaps the future is dragging another lifetime into the picture?

It seems the combination of the future and self parameter causes the problem, not either one taken by itself.

Is there any way to accomplish this?

Your RetExtracter only works for functions that return the same type no matter what lifetime the &B argument has, but that is not the case for functions whose return value borrow from the &B argument. To give a simpler example, consider this function:

pub struct Struct {
    value: u32,
}
impl Struct {
    // These lifetimes could be elided, but I left
    // them in for clarity.
    pub fn f<'a>(&'a self, i: u32) -> &'a u32 {
        &self.value
    }
}

This will fail with a similar error:

error[E0308]: mismatched types
  --> src/lib.rs:35:13
   |
35 |     let e = RetExtractor::new(Struct::f);
   |             ^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected associated type `<for<'a> fn(&'a Struct, u32) -> &'a u32 {Struct::f} as FnOnce<(&Struct, u32)>>::Output`
              found associated type `<for<'a> fn(&'a Struct, u32) -> &'a u32 {Struct::f} as FnOnce<(&'a Struct, u32)>>::Output`

playground

As for whether there's a way to accomplish it, well not the exact thing you asked for. Whether there are acceptable variants would depend on what you are trying to use this for.

2 Likes

This type signature is equivalent to for<'a> Fn(&'a B, u32) -> Ref and asserts that the return type must not depend on the lifetime 'a. In your case, the async fn does however return a future that captures the &self reference, so its anonymous future type does have a lifetime parameter and so that future is a return type that depends on that lifetime of the &B reference.

The way to fix this is to use a custom trait as follows

// trait for async Fns with 2 arguments
pub trait AsyncFn2<A, B>: Fn(A, B) -> Self::Fut {
    type Fut: Future<Output = Self::FutureOutput>;
    type FutureOutput;
}
impl<F: ?Sized, A, B, Fut> AsyncFn2<A, B> for F
where
    F: Fn(A, B) -> Fut,
    Fut: Future,
{
    type Fut = Fut;
    type FutureOutput = Fut::Output;
}

then you can write a trait bound F: for<'a> AsyncFn2<&'a B, u32, FutureOutput = Ret> that has Ret referring to the output type of the future, not the future itself.

Here’s the whole thing in the playground.

1 Like

Thank you both, this is perfect!

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.