HRTB lifetime that spans multiple parameters

Hello. I found another lifetime puzzle while I was refactoring some tests. I have a generic Store type that can change based on a T type that I need to test. Creating T requires borrowing a Store, and T has an implicit internal lifetime.

In a perfect world, the make_t closure would use an internal HRTB to declare the T can't outlive the borrow of the Store. However, that isn't allowed.

Lifting up the lifetime to the outer function means that the lifetime now needs to last to the end of the function, so the owned Store within the function won't work anymore.

pub fn run_test<'a, T: 'a, Store: 'a> (
    make_store: impl Fn() -> Store,
    make_t: impl Fn(&'a Store) -> T,
    test_f: impl Fn(T),
) {
    let store = make_store();
    let t = make_t(&store);
    test_f(t);
}

fn main() {
    run_test(|| -> Vec<usize> {
        vec![42]
    },
    |store: &Vec<usize>| -> &Vec<usize> {
        store
    }, |_t| {});
}

I realize I can lift the creation of the Store outside the function (like this below), but I am hoping there is a solution that lets the closure reference a local variable within the outer function.

pub fn run_test<'a, T: 'a, Store>(
    store: &'a Store,
    make_t: impl Fn(&'a Store) -> T,
    test_f: impl Fn(T)
) {
    let t = make_t(store);
    test_f(t);
}

fn main() {
    let v = vec![42];
    run_test(&v,
    |store: &Vec<usize>| -> &Vec<usize> {
        store
    }, |_t| {});
}

Thanks for any thoughts.

If make_t always returns a reference (or some other type constructor, but always the same one -- Ref<'_, T> say), you can do this.

If not, generalizing over the borrowing return type is technically possible, but tends to completely wreck closure bounds driving closure inference, which you're relying on. I can sketch it out later if you want, but I doubt it's worth it. (You might need a helper function per closure signature to drive inference, at which point why not have a run_test per signature.)

Maybe run_test could be a macro.

Why are make_t and test_t separate if all you do is pass the output of one to the other?

I'm not sure what you meant by this.

1 Like

Thanks for your reply.

If make_t always returns a reference

Sadly T can be a fairly complicated type, and not a straightforward borrow from Store

Why are make_t and test_t separate if all you do is pass the output of one to the other?

They come from different places. test_f is the guts of the test function, while make_t is passed by the caller. Putting the pieces together is the point of run_test

Maybe run_test could be a macro

In fact, in my real code, it is a macro. But I needed to make the macro use a helper function to get the right bounds on the generics.

How are you thinking the macro makes this situation better?

Thanks again.