Async function as an argument - one type is more general than the other

I'm running into an issue when trying to write a wrapper around an async function. I've tried a few different versions of the code, but whether I specify the wrapped function as returning boxed future or I specify it as an async function, the result is pretty much the same. Here's a playground. The code is as follows:

use std::future::Future;
use std::pin::Pin;

struct Foo<'a> {
    foo: &'a str,
}

async fn wrap<'a, F>(mut foo: Foo<'_>, f: F) 
where
  F: FnOnce(&mut Foo<'_>) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
{
  f(&mut foo).await;
}

fn bar(_foo: &mut Foo<'_>) {
    
}

fn wrapped<'a>(foo: &'a mut Foo<'_>) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
    Box::pin(async move {
        println!("foo: {}", foo.foo);
    })
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let s = String::from("foo");
    let foo = Foo {
        foo: &s
    };
    wrap(foo, wrapped).await;
}

If I change the ellided lifetimes of a Foo struct to the wrapper's function lifetime, I don't get the error, but instead s does not live long enough playground. What's curious is that it says that the lifetime introduced by the Pin<Box<dyn Future<....> + 'a>>> is static, not sure why's that the case.

I guess my question is the following: is there any way to make code like this work? Ie. allow to pass a lifetime bound struct to an async function passed as an argument?

Here's a working example by making all lifetimes explicit:

use std::future::Future;
use std::pin::Pin;

struct Foo<'a> {
    foo: &'a str,
}

async fn wrap<'a, 'b: 'a, F>(foo: &'a mut Foo<'b>, f: F) 
where
  F: FnOnce(&'a mut Foo<'b>) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
{
  f(foo).await;
}

fn bar(_foo: &mut Foo<'_>) {
    
}

fn wrapped<'a, 'b: 'a>(foo: &'a mut Foo<'b>) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
    Box::pin(async move {
        println!("foo: {}", foo.foo);
    })
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let s = String::from("foo");
    let mut foo = Foo {
        foo: &s
    };
    wrap(&mut foo, wrapped).await;
}

Thanks a lot, I haven't figured out I have to extend the 'b lifetime if it's specified. Also in some variations I forgot to remove &mut when passing a value that is already &mut :man_facepalming:

One thing that I'm still curious about is: is it possible to shorten lifetime of the FnOnce borrow?

Specifying F: FnOnce(&'a mut Foo<'b> means that the mutable borrow for the function passed as an argument will last at least till the end of the wrap function. Now if I want to do sth like:

  f(foo).await;
  bar(foo).await

I get an error about not being able to mutably borrow foo twice and rightly so.

Is there any way to make that work? I've tried specifying a lifetime just for FnOnce, but it leads to even more errors that are quite cryptic for me at this point.

Yes, you only need to introduce another lifetime:

fn wrapped<'c, 'b: 'a, 'a: 'c>(foo: &'a mut Foo<'b>) -> Pin<Box<dyn Future<Output = ()> + 'c>> {
    Box::pin(async move {
        println!("foo: {}", foo.foo);
    })
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let s = String::from("foo");
    let mut foo = Foo {
        foo: &s
    };
    wrap(&mut foo, wrapped).await;
    bar(&mut foo);
}

Sorry, I was not very precise with my question. I was more talking about situation like in this playground, so when I need to use both wrapped and bar in the wrap function:

use std::future::Future;
use std::pin::Pin;

struct Foo<'a> {
    foo: &'a str,
}

async fn wrap<'c, 'a: 'c, 'b: 'a, F>(foo: &'a mut Foo<'b>, f: F) 
where
  F: FnOnce(&'c mut Foo<'b>) -> Pin<Box<dyn Future<Output = ()> + 'c>>,
{
  f(foo).await;
  bar(foo).await;
}

async fn bar(_foo: &mut Foo<'_>) {
    
}

fn wrapped<'c, 'a: 'c, 'b: 'a>(foo: &'a mut Foo<'b>) -> Pin<Box<dyn Future<Output = ()> + 'c>> {
    Box::pin(async move {
        println!("foo: {}", foo.foo);
    })
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let s = String::from("foo");
    let mut foo = Foo {
        foo: &s
    };
    wrap(&mut foo, wrapped).await;
}

No, sorry. I tried in several ways, but couldn't get it to compile.

Maybe someone else more experienced could provide a more helpful answer.

1 Like

You understand that lifetime parameters must outlive the body; that's good. What you need is the ability to say "this function takes a lifetime shorter than the function body". The way you say that is "this function takes any lifetime", by having a higher-ranked trait bound (HRTB).

I went back to your OP where you're taking Foo<'_> by value. Here's how it looks for that case:

async fn wrap<'a, F>(mut foo: Foo<'a>, f: F)
where
    F: for<'b> FnOnce(&'b mut Foo<'a>) -> Pin<Box<dyn Future<Output = ()> + 'b>>,

The for<'b> ... thing is the HRTB. It says F can take a &'_ mut with any (outer) lifetime 'b.

When lifetime elision applies to a FnOnce(..) trait bound, the elided input lifetimes become higher-ranked...

F: Fn(&str) -> &str
// Same thing
F: for<'s> Fn(&'s str) -> &'s str

...but lifetime elision can't apply to this case since you have multiple input lifetimes and also an output lifetime. You need to give them names so you can specify which lifetime the output has.

This also works for the example given, but is subtly different...

async fn wrap<F>(mut foo: Foo<'_>, f: F)
where
    F: for<'b> FnOnce(&'b mut Foo<'_>) -> Pin<Box<dyn Future<Output = ()> + 'b>>,

// Same as
async fn wrap<'a, F>(mut foo: Foo<'a>, f: F)
where
    F: for<'b> FnOnce(&'b mut Foo<'_>) -> Pin<Box<dyn Future<Output = ()> + 'b>>,

...because F now has to work with Foo<'x> for any lifetime 'x, not just Foo<'a> that matches the Foo<'_> they passed in. I.e. it's more restrictive for the caller.


Now let's see if we can adapt this for the &mut Foo<'_>-taking case...

-async fn wrap<'a, F>(mut foo: Foo<'a>, f: F)
+async fn wrap<'a, F>(foo: &mut Foo<'a>, f: F)
 where
     F: for<'b> FnOnce(&'b mut Foo<'a>) -> Pin<Box<dyn Future<Output = ()> + 'b>>,
 {
-    f(&mut foo).await;
-    bar(&mut foo);
+    f(foo).await;
+    bar(foo);
 }

(Plus adjusting the call site.)

2 Likes

Ah, it's always HRTB where my understanding of lifetimes falls short :see_no_evil:. But thanks to your explanation I think I'm finally grasping the concept :smile:.

2 Likes

Thanks a lot for help @quinedot, now it makes total sense, I think I'm getting a good grasp on how lifetimes work now. I've been using Rust for a while, but usually on server backends and thus I very rarely specify any lifetimes at all :sweat_smile:

Interestingly I tried lifetimes very similar to what you came up with, but I think at that point I had a version of the code with implicit futures, ie. using an async function and then I think it's harder to specify:

async fn wrap<'a, F, Fut>(foo: &mut Foo<'a>, f: F)
where
    F: for<'b> FnOnce(&'b mut Foo<'a>) -> Fut,
    Fut: Future<Output = ()> + 'b,

will result in:

error[E0261]: use of undeclared lifetime name `'b`
  --> src/main.rs:11:32
   |
11 |     Fut: Future<Output = ()> + 'b,
   |                                ^^ undeclared lifetime

Upon further look I found your own reply on another topic that would probably solve this, though! Approaches to an issue with higher-rank trait bounds on another generic type - #2 by quinedot

I'll probably look into it at some point soon and if I do I'll come back with the solution

1 Like

Yeah, using a type parameter like Fut in F: for<'b> FnOnce(&'b mut Foo<'a>) -> Fut usually won't work due to implicitly captured lifetimes. If you can't get the workaround compiling on on your own, feel free to ask :slightly_smiling_face:.