Async, closure, reborrowing and a lifetime issue

Last time I had a serious lifetime issue was before 2018 edition and NLL. And to be honest, I already solved the real problem using a macro... But let's start with the issue. Take the following example code:

use std::future::Future;

struct A {
    count: u32,
}

impl A {
    pub async fn test1(&mut self) {
        self.test_inner(|this| async move { this.count *= 2; }).await;
    }

    pub async fn test2(&mut self) {
        self.test_inner(|this| async move { this.count *= 3; }).await;
    }

    async fn test_inner<'i, 'o: 'i, F, Fut>(&'o mut self, f: F)
    where
        F: FnOnce(&'i mut Self) -> Fut,
        Fut: Future<Output = ()>,
    {
        self.count += 1;
        f(&mut *self).await;
        self.count += 1;
    }
}

Playground

The concept is simple: in the real code I want to perform some HTTP reqwuests in slightly different ways, but each time I want to perform some preliminary operations and, only after the call, I need to modify self. And before you ask: I would like to avoid any sort of internal mutation, because the guarantees given by &mut self is exactly what I want :grin:.

Now, if you try to compile the example, borrowck says that self.count cannot be modified the second time because self is borrowed in the call. My reasoning are the following:

  • The closure is FnOnce, therefore it's used by ownership. In other words, it's dropped before count is changed the second time.
  • await-ing a Future takes the ownership as well, therefore it's dropped before count is changed the second time.
  • The reborrow of self is sort of passed by ownership (I am not sure about the terminology in this case, because self is passed by a mutable re-borrow, but the new borrow is passed by value and it's dropped when the inner call finishes).

I feel my assumptions are right, but this really conflicts with the my mental axiom that compiler is always right. The fact is that async introduces an invisible layer of pinning and abstract state machine to handle yielding and stack so that it could be harder to grasp edge-case lifetime issue and, at the same time, it could be a current limitation of the borrowck.

From the example you can easily understand that it's just a matter of DRY, and this is the reason I already solved the issue with a macro in the real code. On the other hand, normal functions are generally much more ergonomic than macros, and when possible I think that most people prefer to use functions. For this, and because I cannot understand why this code does not work, I would like to listen to suggestions and ideas.

When you write F: FnOnce(&'i mut Self) -> Fut, the lifetime 'i is a specific lifetime chosen by the caller of test_inner, so the body of test_inner is not free to pass in some lifetime that it chooses.

I think what you need is something like this:

F: for<'a> FnOnce(&'a mut Self) -> (impl Future<Output=()> + 'a)

but I don't think there's any way to express this currently. (You can't use -> impl syntax with Fn traits.)

Unfortunately, the only workaround I know is to return a boxed future:

F: for<'a> FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output=()> + 'a>>

Complete example on Playground.

Update: Alternate version on Playground that uses some conveniences from the futures crate to shorten some of the boilerplate code. (This is completely equivalent to the previous example; the differences are only syntactical.)

See also this previous thread.

1 Like

Very good catch, thank you! I did not think about using HRTB in this case, but it totally make sense what you are saying. I need to learn to recognize the need of using this concept in these situations.

The fact that we cannot express an impl Trait + 'a is obviously a bitter limitation, but at the end it is just knowing that when you follow this path, you will have to pay a little price.

Thank you again! :blush:

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.