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!