Calling a generic async function with a (mutably) borrowed argument

When you specify 'a as a generic parameter, you mean “I permit the caller to choose any lifetime it wants”. The caller may as well choose 'static, for example. Then you promise to pass &'a mut i32, that is, &'static mut i32. But i does not live for 'static! That’s the reason for the first error.

The second error is because you’re promising you’re borrowing i mutably for 'a. But again, 'a may as well cover the entire function, even after you discarded the result! The caller may choose 'static, for example, then store the reference in a global variable. If you use i after, you use it while it is mutably borrowed. BOOM!

What you want is not to let the caller choose the lifetime, but instead to say “I’m passing you a reference with some lifetime 'a, and I want you to give me back a future with the same lifetime”. What we use to achieve the effect of “I’m giving you some lifetime, but let me choose which” is called HRTB (Higher-Kinded Trait Bounds).

If you only wanted to return a specific type, not a generic type, it would look like:

async fn call_changer<'a, F, Fut>(changer: F)
where
    F: for<'a> FnOnce(&'a mut i32) -> &'a mut i32,
{ ... }

You can use Box<dyn Future> with this syntax as well:

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

async fn call_changer<F>(changer: F)
where
    F: for<'a> FnOnce(&'a mut i32) -> Pin<Box<dyn Future<Output = ()> + 'a>>,
{
    let mut i = 0;
    changer(&mut i).await;
    dbg!(i);
}

#[tokio::main]
async fn main() {
    call_changer(|i| {
        Box::pin(async move {
            *i = 100;
        })
    })
    .await;
}

Playground.

In fact, you can even get rid of the explicit for clause, since HRTB is the default desugaring for lifetimes in closures:

where
    F: FnOnce(&mut i32) -> &mut i32,
where
    F: FnOnce(&mut i32) -> Pin<Box<dyn Future<Output = ()> + '_>>,

The only question left is: How do we express this with generic Fut?

It’s tempting to try to apply the for<'a> to multiple conditions:

where
    for<'a>
        F: FnOnce(&'a mut i32) -> Fut,
        Fut: Future<Output = ()> + 'a,

Or:

where
    for<'a> FnOnce(&'a mut i32) -> (Fut + 'a),
    Fut: Future<Output = ()>,

But both doesn’t work, unfortunately.

What can we do?

One option is to stay with Pin<Box<dyn Future>>.

Another is to use a custom trait:

trait AsyncSingleArgFnOnce<Arg>: FnOnce(Arg) -> <Self as AsyncSingleArgFnOnce<Arg>>::Fut {
    type Fut: Future<Output = <Self as AsyncSingleArgFnOnce<Arg>>::Output>;
    type Output;
}

impl<Arg, F, Fut> AsyncSingleArgFnOnce<Arg> for F
where
    F: FnOnce(Arg) -> Fut,
    Fut: Future,
{
    type Fut = Fut;
    type Output = Fut::Output;
}

async fn call_changer<F>(changer: F)
where
    F: for<'a> AsyncSingleArgFnOnce<&'a mut i32, Output = ()>,
{
    let mut i = 0;
    changer(&mut i).await;
    dbg!(i);
}

Unfortunately, this does not work with closures. I don’t know why. You have to put a fn:

#[tokio::main]
async fn main() {
    async fn callback(i: &mut i32) {
        *i += 100;
    }
    call_changer(callback).await;
}

Playground.

For more information:

Leave a Comment