I'm trying to create a little utility that simplifies the execution of repetitive work. This would be almost trivial in memory managed languages, but gets unnervingly complex in Rust. I'd call myself an intermediate Rustacean, but I am not yet at a level where the following is something I'm able to solve.
The following example highlights the basic problem:
async fn example_plain() {
let mut check_me = false;
let producer = create_producer(); // assume this was more complex
loop {
if do_thing(producer.next()).await {
check_me = true;
break;
}
}
producer.destroy(); // assume this was more complex
}
async fn do_thing(thing: Thing) -> bool {
true
}
In this example, I would like to abstract the setup and teardown logic of the producer, leaving me able to focus on the significant things. But this gets insanely hard:
async fn do_thing(thing: Thing) -> bool {
true
}
async fn example_better() {
let mut check_me = false;
with_setup(|thing| async {
if do_thing(thing).await {
// Error: captured variable cannot escape `FnMut` closure body
// `FnMut` closures only have access to their captured variables while they are executing...
// ...therefore, they cannot allow references to captured variables to escape
check_me = true;
ExitFlag::Break
} else {
ExitFlag::Continue
}
});
}
async fn with_setup<F, Fut>(mut handler: F)
where
F: FnMut(Thing) -> Fut,
Fut: Future<Output = ExitFlag>,
{
let producer = create_producer();
loop {
match handler(producer.next()).await {
ExitFlag::Break => break,
ExitFlag::Continue => continue,
}
}
producer.destroy();
}
enum ExitFlag {
Break,
Continue,
}
struct Thing {}
struct Producer {}
impl Producer {
pub fn destroy(self) {
// Cleanup logic
}
pub fn next(&self) -> Thing {
Thing {}
}
}
fn create_producer() -> Producer {
Producer {}
}
I believe this would be quite doable using a macro, because it would effectively inline the callback, but I'd like to understand and solve the problem at hand.
You find the full MWE in the playground
Essentially, I wonder if it's possible to tie the lifetime of the future to the lifetime of the captured variables?
This is particularly infuriating as this is only an issue because I need a closure, not just an async block. The entire thing works fine when just wrapped in tokio's timeout
, which is defined as
#[track_caller]
pub fn timeout<F>(duration: Duration, future: F) -> Timeout<F::IntoFuture>
where
F: IntoFuture,
{
let location = trace::caller_location();
let deadline = Instant::now().checked_add(duration);
let delay = match deadline {
Some(deadline) => Sleep::new_timeout(deadline, location),
None => Sleep::far_future(location),
};
Timeout::new_with_delay(future.into_future(), delay)
}
I'm not sure what the significant difference is that makes timeout
with the async block work while the FnMut returning the future fails to compile.
Yes, with the new async
closures:
async fn example_better() {
let mut check_me = false;
with_setup(async |thing| {
// ...
});
}
async fn with_setup<F>(mut handler: F)
where
F: AsyncFnMut(Thing) -> ExitFlag,
{
// ...
}