Lifetimes when wrapping async function in struct

Viewed 246

I'm trying to wrap an async function in a struct. For example:

use std::future::Future;

struct X;
struct Y;

async fn f(x: &X) -> Y {
    Y
}

struct MyStruct<F, Fut>(F)
where
    F: Fn(&X) -> Fut,
    Fut: Future<Output = Y>;

fn main() {
    MyStruct(f);
}

The compiler complains about this with the following (unhelpful) error:

error[E0308]: mismatched types
  --> src/main.rs:16:5
   |
16 |     MyStruct(f);
   |     ^^^^^^^^ one type is more general than the other
   |
   = note: expected associated type `<for<'_> fn(&X) -> impl Future {f} as FnOnce<(&X,)>>::Output`
              found associated type `<for<'_> fn(&X) -> impl Future {f} as FnOnce<(&X,)>>::Output`

Is something like this actually possible? As I understand it, f desugars to something like:

fn f<'a>(x: &'a X) -> impl Future<Output = Y> + 'a {
    Y
}

so I'd need to somehow express in MyStruct that Fut has the same lifetime as x.

1 Answers

I actually don't know much about async.

However, when it comes to trait-bounds, typically, fewer trait-bound are better. In other words, only declare those trait-bounds that you really need.

In the case of a struct, as long as you don't need an associated type within your struct, you are mostly good without any bounds. This is pretty much to what @George Glavan wrote in his answer.

When you add methods to your struct, you are more likely to use traits and thus require trait-bounds. Sometimes it is useful to combine the trait-bound of multiple functions by declaring it on the impl-block itself. Tho, this has some restrictions. You should also consider whether each function really needs all these constraints.

For instance, consider the following code:

struct X;
struct Y;

struct MyStruct<F>(F);

impl<F> MyStruct<F> {
    pub fn new(f: F) -> Self {
        MyStruct(f)
    }

    pub fn invoke<'a, Fut>(&self) -> Fut
    where
        F: Fn(&'a X) -> Fut,
    {
        (self.0)(&X)
    }
}

I added a new and a invoke function. The former doesn't require any traits, thus it doesn't have trait-bounds. The latter only calls the function, so it bounds F by Fn. And this is good enough, because in the end, the caller must already know what the return type is, i.e. whether it is some Future or not.


However, there are a few cases where one really needs additional trait-bounds, which involves additional generics such as for a function return type. In such a case, you can declare additional (phantom) generics on a struct, e.g.:

use std::future::Future;
use std::marker::PhantomData;

struct X;
struct Y;

struct MyStruct<F, Fut> {
    func: F,
    _fut: PhantomData<Fut>,
}

impl<'a, F, Fut> MyStruct<F, Fut>
where
    F: Fn(&'a X) -> Fut,
    Fut: Future<Output = Y> + Send + Sync + 'a,
{
    pub fn new(f: F) -> Self {
        MyStruct {
            func: f,
            _fut: PhantomData,
        }
    }

    pub fn invoke(&self) {
        (self.func)(&X);
    }
}

Notice, that in this example the trait-bounds apply to both function new and invoke, and both are over-restricted. Still, you don't need to over-restrict the struct itself.

Related