Are the lifetimes necessary?

Hi,

I've written a crate to run coroutines as a data driven data structure. However when it comes to providing the 'continuation' or 'resume when fed a new input', my implementation of 'bind' from a monad point of view gets pretty lifetimey pretty quickly.

bicoro/coroutine.rs at main · btrepp/bicoro · GitHub

I'm wondering if there is a way of writing this without the lifetimes?.
I'm also wondering if there is a way to write without the Box as much, I think it is required, but would be nice if there was progressive enhancement, eg if you had a dumb fn pointer it would work in a no-std context, but you can use Box if you run in std and have an allocator.

Appreciate any pointers on the heap and lifetimes, I've sort of 'fought the compiler until it compiles', and the code works for the use-cases I had, but I feel like it isn't expressed correctly, so maybe is more difficult to read than it needs to be.

Thanks :slight_smile:

Well, do you want it to be possible for the Box<dyn FnOnce ...> to contain any references to local variables? If you do want that to be possible, then you do need the lifetime, and the lifetime becomes the duration in which the value in the box borrows from those local variables.

The caller can set the lifetime to 'static if the FnOnce closure doesn't contain any references.

If you don't want to allow references inside the closure, then you can simply remove the lifetimes. (And put : 'static on all types that go inside the box.)

1 Like

I'm not 100% sure what I want really from the lifetimes. It's just a sort of 'this works'.

From my point of view, they seem to exist to allow a continuation. E.g when the next "Input" arrives, run this function. I don't mind restricting the caller somewhat, but I feel like if the inputs all become 'static, that's probably not the best program.

Ideally if there was a way to express the binder, F, to be a closure, but it can all be on the heap to avoid a lifetime?, that would be great. I think I am hitting lifetime problems because if someone passes a closure, obviously that needs to live long enough for the 'arbitrary later date' that it gets evaluated.

What I would really like is for my structure to 'own' the closure, and the closed over variables, in a read-only fashion. It should be 'immutable' and clone able from my perspective. However I am struggling to get the compiler to be happy with this.

pub fn bind<I, O, RA, RB, F>(m: Coroutine<I, O, RA>, f: F) -> Coroutine<I, O, RB>
where
    F: Copy + FnOnce(RA) -> Coroutine<I, O, RB>,
{
    match m.resume {
        CoroutineState::Done(ra) => f(ra),
        CoroutineState::Yield(output, ra) => {
            let state = bind(*ra, f);
            let resume = CoroutineState::Yield(output, Box::new(state));
            Coroutine { resume }
        }
        CoroutineState::Await(ra) => {
            let state = move |input: I| -> Coroutine<I, O, RB> {
                let next = ra(input);
                bind(next, f)
            };
            let resume = CoroutineState::Await(Box::new(state));
            Coroutine { resume }
        }
    }
}

At the moment I've simplified it to the above, but F,I etc don't live long enough. This confuses me a bit, as I am happy to have them be 'owned', by the coroutine structure, but I can't figure out how to do that. I would have thought liberal use of copy/clone/box is making me responsible for them, but the compiler is still not happy :slight_smile:

EDIT:

I worked on it more, and I think the key is in my suspend function

pub fn suspend<I, O, R, F>(f: F) -> Coroutine<I, O, R>
where
    F: Fn(I) -> Coroutine<I, O, R> + Copy,
{
    let resume = CoroutineState::Await(Box::new(f));
    Coroutine { resume }
}

E.g If I can figure out how to pass a closure, F, and store that completely owned and on the heap, I will be good. It's almost like I want a more restricted Fn closure, in which none of the values are borrowed, they are all owned. Unsure if this trait exists.

That's exactly what T: 'static does.

1 Like