How to define async methods without capturing lifetime of self

I have some code I wanted to move to async-await. Here the NamedFuture struct doesn't capture any lifetime.

impl Struct {
    fn foo(&self) -> NamedFuture {
        // do stuff
        self.bar()
    }

    fn bar(&self) -> NamedFuture {
         NamedFuture::new()
    }
}

But changing this to use async-await:

impl Struct {
    async fn foo(&self) {
        // do stuff
        self.bar().await
    }

    async fn bar(&self) {
         NamedFuture::new().await
    }
}

The async-version captures the lifetime of self which breaks lots of existing uses such as:

let fut = {
    let s = Struct::new();
    s.foo();
}

or:

let s = Struct::new();
let fut = s.foo();
tokio::spawn(fut);

Can we somehow indicates or the compiler can infer that the returned future doesn't need to capture self and bind to its lifetime?

Real world use case: Hyper's Client::get currently returns ResponseFuture which doesnt capture self.

There is no way to annotate an async fn to not capture a lifetime in it, and inferring that would go against Rust's current philosophy that function signature's should be able to be relied on independently of the body (other than auto-trait leakage in RPIT).

You can instead make bar a normal fn returning an impl Future and use an async block to define it:

impl Struct {
    fn bar(&self) -> impl Future<Output = ()> {
        async move {
            NamedFuture::new().await
        }
    }
}

You then have all the normal capabilities of RPIT to accurately define which lifetimes the return type captures.

6 Likes

Yes that's a possible solution but it turns ugly when you have branching code. It's a bunch of nested Eithers.

I had a relief when I changed method to async. The code became very clean, but it doesnt work as expected.

The body of an async block is basically identical to the body of an async fn (and you can see the latter as a desugaring to the former), you should be able to write it exactly the same as you would an async fn and just move the code inside the block.

2 Likes

At some places, the branch conditions are self's fields. If I move that condition inside async block, self is captured. If I keep it out of it, I can't write the code without Either.

I guess we could move the "async" part of the code into free functions/async blocks and pass "copied or cloned state" to the them.

free functions will be better because of the better error handling support.

Will the situation be better with async closures? They produce future on executing them right?

Async non-move closures are intended to have the same signature lifetime handling of async fn, so are unlikely to help. Current async move |_| { ... } closures are (as far as I'm aware) identical to closures returning async blocks (|_| async move { ... }).

1 Like

Thanks @Nemo157. I think I can use RPIT.

The easiest way to do this is to compute the branch conditions before the block, assign them to bool variables, and then have the async block capture those variables and not self.

3 Likes

Might be too late, but could this be an argument for not stabilizing async functions and instead relying only on async blocks, plus maybe a syntax to capture all lifetimes in the return type, so that the user has to explicitly make this choice? (e.g. fn foo([...]) -> impl Future<[...]> + '* )

1 Like

Async fn are useful in plenty of situations. I don't see why they should not be stabelized. Note that capturing the lifetime of self is problematic if you want to directly spawn the future on an executor (which usually requires a 'static future.

If you have an async context that owns an object, it can perfectly await methods on that object. Your outer async context will still be 'static and can be spawned.

The future has to capture lifetimes, since it's not run immediately, and the objects need to be around when it does. No different than any other struct that owns references to things.

1 Like

@withoutboats Yes thats the approach I am taking now. But it doesnt always feel as natural as writing an regular async fn.

@najamelan Thing about letting self captured is that some of the code store the future. I ll be propagating lifetime to very large scope.

@bill_myers I dont think this is reason enough to block stablization of async fns. This problem can probablt solved better with a different design. My question is if something can be done for now. Most codebases doing these things will probably have to redesign around this or use RPIT or custom futures. Tokio already removed some of the poll_* functions in favour of async ones. So users will have to do the same.

Obviously but there's no way around this. You can't use self within the async scope without capturing it, if you don't want to capture it you have to precompute whatever values you need before the async context is constructed. What you are writing is exactly what you want the program to do.

3 Likes

Thanks @withoutboats, I think I understand it now. Thanks all for your help.

2 Likes