5

I am trying to create a async function which takes a function pointer as a parameter. It does some stuff, calls the function, awaits on the result, then does some more stuff:

use std::future::Future;

async fn run_another_async_fn<F, Fut>(f: F)
where
    Fut: Future<Output = ()>,
    F: FnOnce(&mut i32) -> Fut,
{
    let mut i = 42;
    println!("running function");
    f(&mut i).await;
    println!("ran function");
}

async fn foo(i: &mut i32) {}

async fn bar() {
    run_another_async_fn(foo);
}

[view on Rust Playground]

Unfortunately this fails to compile:

error[E0308]: mismatched types
  --> src/lib.rs:17:5
   |
17 |     run_another_async_fn(foo);
   |     ^^^^^^^^^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected associated type `<for<'_> fn(&mut i32) -> impl Future {foo} as FnOnce<(&mut i32,)>>::Output`
              found associated type `<for<'_> fn(&mut i32) -> impl Future {foo} as FnOnce<(&mut i32,)>>::Output`
   = note: the required lifetime does not necessarily outlive the empty lifetime
note: the lifetime requirement is introduced here
  --> src/lib.rs:6:28
   |
6  |     F: FnOnce(&mut i32) -> Fut,
   |                            ^^^

Firstly, it seems the compiler found exactly what it expected but it's complaining anyway?

Secondly, what's "the empty lifetime"? I guess it must mean the '_, does that have some special significance?

Finally, what's the way to get this to compile?

2
  • Possibly related: stackoverflow.com/questions/67991159/… Commented Aug 22, 2021 at 15:52
  • 3
    @sk_pleasant It looks like the solution there won't work for this, because f is given a borrow of a local variable run_another_async_fn — so its type F cannot be declared to require a lifetime that's a parameter of run_another_async_fn, which would outlive that local. Commented Aug 22, 2021 at 18:05

1 Answer 1

1

The issue is that there is no way to specify the same lifetime for F and Fut in the where clause.

Luckily (if you don't mind heap allocating the future) there is an easy workaround. You can use the already existing futures::future::BoxFuture; which looks like:

pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

With its help you can specify the same lifetime parameter for both the borrow and as a trait bound for the future:

 where  for<'a> F: FnOnce(&'a mut i32) -> BoxFuture<'a, ()>,

You also have to add an adapter function which will have the correct return type - i.e. BoxFuture<'_, T> instead of impl Future:

fn asd(i: &mut i32) -> BoxFuture<'_, ()> {
    foo(i).boxed()
}

or use a closure:

run_another_async_fn(|i| foo(i).boxed());

As a result your code would look like:

use futures::future::BoxFuture;
use futures::FutureExt;
use std::future::Future;

async fn run_another_async_fn<F>(f: F)
where
    for<'a> F: FnOnce(&'a mut i32) -> BoxFuture<'a, ()>,
{
    let mut i = 42;
    println!("running function");

    f(&mut i).await;

    println!("ran function");
}

fn asd(i: &mut i32) -> BoxFuture<'_, ()> {
    foo(i).boxed()
}

async fn foo<'a>(i: &'a mut i32) {
    // no-op
}

async fn bar() {
    run_another_async_fn(asd);
    run_another_async_fn(|i| foo(i).boxed());
}
Sign up to request clarification or add additional context in comments.

8 Comments

It seems odd to me that I need to allocate on the heap, because if I just call foo directly rather than calling it via a function parameter there's no problem, and as far as I can tell, no heap allocation either. Is there something about a function pointer that changes the requirements? Or is this a documented language limitation?
@PhilFrost 1. Your F is not a function pointer, but a generic closure 2. Every closure, even if it has the same arguments and return type is a distinct type, so ||{...} is distinct from another ||{...} See this SO question 3. Given 1 and 2, when you are passing a closure, the compiler will generate a different function, one for each different generic type. While when you call it directly - you have only one function
It's not about being heap vs stack allocated. The issue is that there is no way to tell that F and Fut have the same lifetime restrictions in the where just as Kevin Reivd has commented under your question
I saw that, but why? This seems like a pretty basic thing for a language which ostensibly supports higher-order functions and async programming.
I guess what I'm saying is I do mind heap allocating the future. If I just wanted to heap allocate things even when there was no particular need to do so, I would use any number of interpreted languages which do exactly that. So I'm having a hard time accepting that this is "just how it is".
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.