Function that takes an async closure

I'm trying to implement a function that takes an async closure, but I don't know how to tell the compiler that the future resulting from the closure will be resolved before the closure argument from the enclosing function goes out of scope. Any help appreciated!

use tokio; // 1.7.1
use futures::future::Future;

#[derive(Debug)]
struct Foo {
    
}

async fn takes_closure<Fut: Future<Output = ()>>(f: impl Fn(&mut Foo)->Fut) {
    let mut foo = Foo{};
    f(&mut foo).await;
}

#[tokio::main]
async fn main() {
    takes_closure(|foo| async {
        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
        println!("hello {:?}", foo);
    }).await;
}

(Playground)

  1. The input lifetime of the borrow over Foo is local to the callee,

  2. Thus, a higher-order signature is needed: impl for<'local> Fn(&'local …)…

  3. But when writing an async closure such as:

    |foo: &'_ mut Foo| async move { … }
    

    the value returned by that closure is the type of the async move { … } future, which captures foo, and thus, whose type is infected with the lifetime of foo.

    That is, to correctly express the bounds of the closure, you'd need to write:

    f: impl for<'local>
        Fn(&'local mut Foo) -> impl 'local + Future<…>
    
  4. You have, however, used a fixed generic parameter Fut to represent the return type of that closure, that is you expected to have:

    Fut = impl 'local + Future<…>
    

    but then, what is 'local there? And there is no valid answer there: 'local doesn't make sense for Fut since, as a fixed generic parameter, its type is defined before the higher-order / universal quantification for<'local> ever gets introduced.

  5. Hence the error.

The solution

is then to express that nested impl bound. Alas, this is not something that can be done on stable Rust, and even if it were, there would be the issue that the bound on the return type (that of being a Future<Output = ()>) usually leads to weird type-checker / trait solver errors, due to limitations in the compiler (lazy normalization bugs, to be exact).

  • For async fn only (not for async closures!), using a helper trait such as:

    trait MyAsyncFn<'foo> {
        type Fut : Future<Output = ()>;
        fn call (
            self: &'_ Self, // Fn
            _: &'foo mut Foo,
        ) -> Self::Fut;
    }
    
    impl<'foo, Fut : Future<Output = ()>, F : Fn(&'foo mut Foo) -> Fut>
        MyAsyncFn<'foo>
    for
        F
    {
        type Fut = Fut;
        fn call (
            self: &'_ Self, // Fn
            foo: &'foo mut Foo,
        ) -> Fut
        {
            self(foo)
        }
    }
    
    • (And then using impl for<'local> MyAsyncFn<'local>, and using f.call(&mut foo).await to call it)

    does work: Playground. But for || async move { … } closures, this approach still runs into issues with the "async closure" definition not being imbued with the right higher-order signature. Maybe in the future with proper async move || { … } this solution would work :pray:.

Hence, the only reliable workaround for those bugs with |…| async move { … } closures (and, incidentally, around the nightly requirement): to replace

-> impl '_ + Future<…>

with

-> dyn '_ + Future<…>

And the latter requires pointer indirection, leading to:

f: impl Fn(&'_ mut Foo) -> Pin<Box<dyn '_ + Future<Output = ()>>>

or, to be more concise (and also get a nice Send bound thrown into the mix):

use ::futures::future::{BoxFuture, FutureExt};

f: impl Fn(&'_ mut Foo) -> BoxFuture<'_, ()>

Note that in order for the async closure to work, adding a .boxed() call on the async move { … } block is necessary:

use ::futures::future::{BoxFuture, FutureExt};

#[derive(Debug)]
struct Foo {}

async fn takes_closure (f: impl Fn(&'_ mut Foo) -> BoxFuture<'_, ()>)
{
    let mut foo = Foo {};
    f(&mut foo).await;
}

#[::tokio::main]
async fn main ()
{
    takes_closure(|foo: &'_ mut Foo| async move {
        ::tokio::time::sleep(::std::time::Duration::from_millis(10)).await;
        println!("hello {:?}", foo);
    }.boxed()).await;
}
6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.