Generalising lifetimes of coroutines

Hey y'all, this is my first post. I'm very excited to finally be here. :wave:

I'm playing around with coroutines (on nightly) and ended up in a situation where I would like my coroutine to have mutable access to some data while it is running but not hold on to the reference when it is suspended. So, I came up with the idea of passing a mutable reference into the coroutine when resuming it and handing it back when yielding/returning. The following code compiles without error and works as intended:

type Coro<'a, T> =
    Pin<Box<dyn Coroutine<&'a mut T, Yield = &'a mut T, Return = &'a mut T> + 'a>>;

type GenCoro<T> =
    Pin<Box<dyn for<'a> Coroutine<&'a mut T, Yield = &'a mut T, Return = &'a mut T>>>;

fn coro_fn<'a>(_: PhantomData<&'a ()>) -> Coro<'a, String> {
    Box::pin(
        #[coroutine]
        static |mut data: &mut String| {
            while !data.is_empty() {
                data = yield data;
                data.pop();
            }
            data
        },
    )
}

(You can ignore the PhantomData for now, it only becomes relevant later.)

However, the lifetime parameter 'a of Coro is a little annoying. If I'm not mistaken, the Coro returned by coro_fn can't actually hold on to any data with lifetime 'a. Thus, I would love to generalise over the 'a and the return type of coro_fn to be GenCoro<T>. The borrow checker doesn't seem to happy with this though. It complains about the implementation of Coroutine not being general enough as it must implement the trait for all lifetimes but only does so for some specific anonymous lifetime.

My first question is whether transmuting the result of coro_fn into GenCoro<T> is safe? My intuition about parametricity (in 'a), affine types, String string being 'static, and the coroutine closure being static (as opposed to move) tells me that this is safe indeed. Or did I miss anything?

In case this is safe, I would—of course—love to capture the pattern in a safe higher-order function. I came up with the following combinator:

fn generalize<T, F>(f: F) -> GenCoro<T>
where
    T: 'static,
    F: for<'a> Fn(PhantomData<&'a ()>) -> Coro<'a, T>,
    F: 'static,
{
    unsafe { std::mem::transmute::<_, GenCoro<T>>(f(PhantomData)) }
}

So, my second question is whether the transmute in this function is always safe under the given constraints. Again, my intuition says "yes" but I'm well aware that I might be missing something. Is there actually a way to relax the constraints in order to make the function usable in more situations?

I'm very much looking forward to your answers/suggestions/etc. Many thanks!

This seems relevant

1 Like