Approaches to an issue with higher-rank trait bounds on another generic type

During my usual Rust lifetime abuse I stumbled upon a problem with higher-rank trait bounds, making the following kind of requirement seemingly impossible to specify:

trait Test {}
struct TestImpl<'a>(&'a i32);
impl<'a> Test for TestImpl<'a> {}

fn create_and_use<F, T: Test>(ctx: i32, factory: F)
where
    F: for<'any> Fn(&'any i32) -> T // + 'any ???
{
    let instance = factory(&ctx);
    todo!()
}

fn test() {
    create_and_use(0, |ctx| TestImpl(ctx));
}

Simply put: create_and_use takes some kind of context (represented here as i32) and a factory Fn which returns some impl of Test. As TestImpl<'a> borrows the context, we have to specify that in the bound for F: "The Fn takes an 'any context and returns some T which borrows the 'any context for its lifetime."

Without adding some kind of bound, the compiler complains as expected:

error: lifetime may not live long enough
   |
17 |     create_and_use(0, |ctx| TestImpl(ctx));
   |                        ---- ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                         |  |
   |                         |  return type of closure is TestImpl<'2>
   |                         has type `&'1 i32`

The following gets the point across but obviously does not work:

F: for<'any> Fn(&'any i32) -> impl Test + 'any

Note that this compiles when boxing the return value and using dynamic dispatch, however this seems rather inelegant:

F: for<'any> Fn(&'any i32) -> Box<dyn Test + 'any>

I've found a few forum posts discussing this in the context of futures. A solution by @Yandros (Accepting an async closure + lifetimes = stumped - #8 by Yandros) suggests using the internal representation of the Fn trait like this:

fn create_and_use<F>(ctx: i32, factory: F)
where
    for<'any> F: Fn<(&'any i32)>,
    for<'any> <F as FnOnce<(&'any i32)>>::Output: Test + 'any,
{
    let instance = factory.call((&ctx));
    todo!()
}

This requires using a nightly compiler and enabling the features unboxed_closures to get rid of the error preventing us from using the internal Fn representation, as well as fn_traits to use the call(...) fn (error[E0059]: cannot use call notation; the first type parameter for the function trait is neither a tuple nor unit).

In addition to these drawbacks, this still does not compile:

error[E0277]: the trait bound `for<'any> <_ as FnOnce<&'any i32>>::Output: Test` is not satisfied
   |
29 |     create_and_use(0, |ctx| TestImpl(ctx));
   |     --------------    ^^^^^^^^^^^^^^^^^^^ the trait `for<'any> Test` is not implemented for `<_ as FnOnce<&'any i32>>::Output`
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `Test` is implemented for `TestImpl<'a>`

I can't really make sense of that error unfortunately.
Another solution by @steffahn (Higher-Rank Trait Bounds use bound lifetime in another generic - #2 by steffahn) proposes using a "helper trait" like this:

trait TestFn<'a> {
    type Output: Test + 'a;
    fn tcall(&self, ctx: &'a i32) -> Self::Output;
}

impl<'a, F, R> TestFn<'a> for F
where
    F: Fn(&'a i32) -> R,
    R: Test + 'a {
    type Output = R;

    fn tcall(&self, ctx: &'a i32) -> Self::Output {
        self(ctx)
    }
}

fn create_and_use<F: 'static + for<'any> TestFn<'any>>(ctx: i32, factory: F) {
    let instance = factory.tcall((&ctx));
    todo!()
}

Also looks like it should work, however it does not:

error: implementation of `TestFn` is not general enough
   |
55 |     create_and_use(0, move |ctx| TestImpl(ctx));
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `TestFn` is not general enough
   |
   = note: `[closure@src/lib.rs:55:23: 55:33]` must implement `TestFn<'0>`, for any lifetime `'0`...
   = note: ...but it actually implements `TestFn<'1>`, for some specific lifetime `'1`

I'm completely at a loss. Is there really no way to specify this kind of requirement without resorting to dyn? I'd appreciate any suggestions. Thanks for reading this!

The helper trait works, but then you're running into an annoying (but common) closure inference problem.

I worked around it with a helper function; someday there will be a more ergonomic solution.


As an explanation for why you need the helper trait, or the ability to name Fn<(...)> without mentioning <_ as Fn<...>>::Output... Here:

fn create_and_use<F, T: Test>(ctx: i32, factory: F)
where
    F: for<'any> Fn(&'any i32) -> T

Type parameters like T must resolve to a single concrete type. But types that differ by lifetime only are still distinct types. You need some sort of type constructor by way of an associated type. (By way of an associated type because Rust doesn't have generic type constructors.)

Here's a more generic post on the topic.

2 Likes

Makes total sense. Thanks for your help @quinedot!

In the meantime, there is

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.