Just something I've run into and have been thinking about. I'm not precisely sure how much it would impact the API (probably too much!), and what the advantages and disadvantages are, or any of the workarounds, but I figured the maintainer might have some thoughts to add on the matter.
Basically: What might it take to be able to get the "resume" type (R
) to support short-lived, non-overlapping lifetimes?
Consider the following program, which implements an event loop as an internal iterator.
pub fn main() {
event_loop(event_handler());
}
pub struct Knob;
impl Knob {
pub fn get_from_somewhere() -> Self { Knob }
pub fn frobnicate(&mut self) { println!("tweaking knob"); }
pub fn use_somehow(&self) { println!("using knob somehow"); }
}
pub struct Event<'a> { knob: &'a mut Knob }
pub enum LoopStatus<T> { Continue, Break(T) }
pub fn event_loop<T>(
mut callback: impl FnMut(Event<'_>) -> LoopStatus<T>,
) -> T {
loop {
let mut knob = Knob::get_from_somewhere();
match callback(Event { knob: &mut knob }) {
LoopStatus::Continue => {},
LoopStatus::Break(x) => return x,
}
knob.use_somehow();
}
}
pub fn event_handler() -> impl FnMut(Event<'_>) -> LoopStatus<()> {
// Do something, I dunno, implement a finite state machine or something.
// Here's a very not interesting example.
let mut times_to_frobnicate = 3i32;
move |Event { knob }| {
if times_to_frobnicate > 0 {
times_to_frobnicate -= 1;
knob.frobnicate();
LoopStatus::Continue
} else {
LoopStatus::Break(())
}
}
}
Of course, the manually writing a state machine is a pain. So, coroutines to the rescue!... right?
use genawaiter::{GeneratorState, yield_};
use genawaiter::stack::{Co, Gen, let_gen_using};
use std::future::Future;
pub fn main() {
let_gen_using!(gen, |co| event_handler(co));
event_loop(gen);
}
pub struct Knob;
impl Knob {
pub fn get_from_somewhere() -> Self { Knob }
pub fn frobnicate(&mut self) { println!("tweaking knob"); }
pub fn use_somehow(&self) { println!("using knob somehow"); }
}
pub struct Event<'a> { knob: &'a mut Knob }
pub fn event_loop<T>(
gen: &mut Gen<'_, (), Option<Event<'_>>, impl Future<Output=T>>,
) -> T {
gen.resume_with(None);
loop {
let mut knob = Knob::get_from_somewhere();
match gen.resume_with(Some(Event { knob: &mut knob })) {
GeneratorState::Yielded(()) => {},
GeneratorState::Complete(x) => return x,
}
knob.use_somehow();
}
}
pub async fn event_handler(co: Co<'_, (), Option<Event<'_>>>) {
let times_to_frobnicate = 3i32;
for _ in 0..times_to_frobnicate {
let Event { knob } = co.yield_(()).await.unwrap();
knob.frobnicate();
}
co.yield_(()).await.unwrap(); // make sure the last knob is used
}
Unfortunately, this fails to build:
error[E0597]: `knob` does not live long enough
--> src\bin\event-loop.rs:25:50
|
20 | gen: &mut Gen<'_, (), Option<Event<'_>>, impl Future<Output=T>>,
| -- let's call this `'1`
...
25 | match gen.resume_with(Some(Event { knob: &mut knob })) {
| -----------------------------------^^^^^^^^^----
| | |
| | borrowed value does not live long enough
| argument requires that `knob` is borrowed for `'1`
...
30 | }
| - `knob` dropped here while still borrowed
Knowing how Fn
traits desugar, the reason why the latter fails while the former succeeds is fairly evident:
- The original event handler was
impl for<'a> FnMut(Event<'a>)
. Thus, every call of the function could use a distinct lifetime.
- The new event handler is
Gen<'_, (), Option<Event<'a>>>
for a single lifetime 'a
. This single lifetime is forced to encompass all calls to gen.resume_with
.
Typically, the solution to this is to try to either introduce some trait with a lifetime parameter (so that we can have for<'a> Trait<'a>
), or to introduce a trait with a method that has a lifetime parameter (trait Trait { fn method<'a>(...) { ... }}
). But that's only a very vague plan... I'm not entirely sure where either of these techniques could be applied to the design of genawaiter!