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;
}
}
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 .
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 beforecount
is changed the second time. -
await
-ing aFuture
takes the ownership as well, therefore it's dropped beforecount
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, becauseself
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.