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?

Phil Frost
  • 3,668
  • 21
  • 29
  • Possibly related: https://stackoverflow.com/questions/67991159/lifetime-of-async-closure-return-type – Elias Holzmann Aug 22 '21 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. – Kevin Reid Aug 22 '21 at 18:05

1 Answers1

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());
}
Svetlin Zarev
  • 14,713
  • 4
  • 53
  • 82
  • 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? – Phil Frost Aug 23 '21 at 03:49
  • @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](https://stackoverflow.com/questions/27874683/types-of-unboxed-closures-being-unique-to-each) 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 – Svetlin Zarev Aug 23 '21 at 07:08
  • 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 – Svetlin Zarev Aug 23 '21 at 07:16
  • I saw that, but why? This seems like a pretty basic thing for a language which ostensibly supports higher-order functions and async programming. – Phil Frost Aug 25 '21 at 14:41
  • 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". – Phil Frost Aug 25 '21 at 14:44
  • https://internals.rust-lang.org/t/how-to-talk-about-lifetime-of-opaque-fnonce-result/13783/5 seems to offer a solution not involving Box – Phil Frost Aug 25 '21 at 21:16
  • @PhilFrost the trait based solution offered in internals is pretty good albeit verbose, maybe you can add it as an alternative answer to your own question? – franleplant Aug 30 '21 at 16:05
  • @franleplant while the solution does work, I can't confidently answer my own question because I don't really understand what the solution is solving, which is especially difficult because the compiler is complaining that it found exactly what it was expecting. – Phil Frost Sep 01 '21 at 15:09