Identifying the problem
async move |i: &'_ mut Int| {
...
}
is sugar for
// +---------+
// | | captured inside the `async { ... }` Future.
// vvvvvvvvvvv v
move |i: &'_ mut Int| async move {
...
}
That is, is we call 'i the lifetime of the borrow over i, the returned async move { ... } block / impl Future type is infected with that 'i lifetime.
- Returned type is of the form:
CompilerGeneratedFutureFromAsyncBlock<'i, ...>
And that is not what the impl FnOnce(&mut Int) -> F where F : Future... expresses.
If we forget about the F : Future bound, the trait bound imposed on the closure is:
(&'_ mut Int) -> F
// i.e.
// there exists some single type `F` so that
for /* all */ <'i> (&'i mut Int) -> F
Now, a type F infected with a <'i> lifetime (such as in your actual call) will never be able to meet that criteria:
- if
F = CompilerGeneratedFutureFromAsyncBlock<'i, ...>, then when 'i changes, so does F, so the signature of the closure does not match that of a fixed return type _w.r.t. 'i.
Another way of seeing this, is that the (&'_ mut Int) -> F signature is expressing that F does not depend on the borrow of the input Int, which means that the following would type check:
fn does_typecheck<F> (
closure: impl FnOnce(&mut Int) -> F,
) -> F
where
F : Future,
{
{
let mut int: Int = ...;
let f = closure(&mut int);
f
} // drops(int)
} // returns f
And then, if your actual closure happened to be accepted for this signature, the returned future would be dereferencing a dangling &mut Int when .await-ed the first time.
Fixing the problem
Now that we have identified the issue (F does not depend on the 'i lifetime of the input &'i mut Int), we can fix it.
-
fn perform<'i, F>... would allow F to depend on 'i, but at the cost of requiring an outer lifetime parameter, which by design must outlive the return point of the function, which your let mut i short-lived Int does not: you were correct in requiring what is called a HRTB (higher-rank trait bound): impl for<'i> FnOnce(&'i mut Int) ....
-
In an ideal world, the solution would be being able to write something along the lines of:
impl for<'i> FnOnce(&'i mut Int) -> F<'i>,
where
for<'i> F<'i> : Future<Output = ()>,
-
But that doesn't work with a generic parameter F, since F is expected to be a type itself / Rust does not accept feeding <'param> to type parameters (that would require HKT (higher-kinded types), or, if we allow ourselves to drop some sugar and use intermediate types, a weaker form of it, GAT, (generic associated types) should theoretically solve this. In practice, however, it doesn't: the trait solver isn't currently smart enough to reason at that level of abstraction, and it gets confused.
This means that in practice, we can no longer be that generic (over F I mean). We'll have to use a concrete type. But what kind of concrete type can be used with an async block? Well, the dynamic / runtime-dispatched type-erased future: Pin<Box<dyn Future<Output = ()> + 'lifetime>>, conventienty aliased as BoxFuture<'lifetime, ()> within the ::futures crate:
impl Test {
async fn perform (
self: &'_ Self,
action: impl for<'i> FnOnce(&'i mut Int) -> BoxFuture<'i, ()>,
)
{
let mut i = Int { i: 0 };
let i_ref: &mut Int = &mut i;
action(i_ref).await;
}
}
#[::tokio::main]
async fn main ()
{
let test: Test = Default::default();
let mut other = Int { i: 0 };
let other_ref: &mut Int = &mut other;
test.perform(move |i: &mut Int| Box::pin(async move {
something().await;
// Why is this okay?
other_ref.i = 1;
// But not this? Commenting out fixes the code...
i.i = 1;
})).await;
}
Sadly the above now fails with:
error[E0597]: `other` does not live long enough
--> src/main.rs:29:31
|
29 | let other_ref: &mut Int = &mut other;
| ^^^^^^^^^^ borrowed value does not live long enough
30 | test.perform(move |i: &mut Int| Box::pin(async move {
| _____________________________________-
31 | | something().await;
32 | |
33 | | // Why is this okay?
... |
37 | | i.i = 1;
38 | | })).await;
| |______- returning this value requires that `other` is borrowed for `'static`
39 | }
| - `other` dropped here while still borrowed
Why? Well, while we have fixed the issue with the 'i lifetime, your closure also happens to capturing an outer ref that is not 'static, so the closure itself is also infected with an 'outer lifetime, which, in turn, infects the future too.
And the problem is, given the HRTB signature we have used, when 'i = 'static (even though it shall never be the case in practice), the future must be allowed to be + 'static, meaning that if it captures any 'outer thing, then 'outer >= 'static, so 'outer = 'static. So, again, we have overconstrained our signature, and it does not accept your use case.
Fixing this, in theory, could be easy:
-
we start by adding a classic lifetime parameter <'outer> to our perform function, and require that both the closure, and the future it generates outlive it:
-
BoxFuture only takes a single lifetime parameter, and 'i + 'outer isn't a lifetime parameter.
-
If we replace the type alias by its associated PinBox-ed trait object we should be able to solve this, right?
Pin<Box<dyn 'i + 'outer + Future<Output = ()>>> // must outlive _both_ `'i` and `'outer`
Well, it turns out, that for some reason / arbitrary limitation, trait objects cannot carry multiple lifetime bounds, even though it would be the perfect solution here 
-
Adding a where 'outer : 'i (meaning 'outer >= 'i) and then using 'outer in our BoxFuture would solve everything, but we cannot have where bounds with for<...> parameters yet ... 
At this point, I've been quite bummed that for such a simple initial problem we have reached some form of language limitation deadlock / dead end...
Except ... that implicit where bounds can be applied to all parameters, including for ones!
This means that the following works 
/// Introduce the implicit 'snd : 'fst bound
struct Hack<'short, 'long : 'short, R = ()> (
BoxFuture<'short, R>,
PhantomData<&'long ()>,
);
impl Test {
async fn perform<'other>(
self: &'_ Self, // where 'other : 'i 🤯
action: impl 'other + for<'i> FnOnce(&'i mut Int) -> Hack<'i, 'other, ()>,
)
{
let mut i = Int { i: 0 };
let i_ref: &mut Int = &mut i;
action(i_ref).0.await;
}
}
#[::tokio::main]
async fn main ()
{
let test: Test = Default::default();
let mut other = Int { i: 0 };
let other_ref: &mut Int = &mut other;
test.perform(move |i: &mut Int| Hack(Box::pin(async move {
something().await;
other_ref.i = 1;
i.i = 1;
}), PhantomData)).await;
}