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".
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.