That part could be considered a shortcoming, I think. At least, I'm pretty sure the compiler could soundly draw some conclusions due to the 'static
restraints and not capture the lifetime in the definition of the RPITIT.
As was pointed out we don't have precise capturing in traits yet, and that would solve this problem. But IMO your code should compile now anyway, even with the lifetime capture. That is to say, even with the lifetime capture, there's a bug or shortcoming here.
Let me drill down some more to explain what I mean and where the error is coming from.
Because the RPITIT captures the implementing type and lifetime, it notionally acts like this:
// Conceptually how the RPIT is defined. `Self` can be considered
// an input to any given implementation (along with the lifetime `'a`)
trait ComponentRpit<'a> {
type Create: Any;
}
pub trait Component: 'static + for<'a> ComponentRpit<'a> {
fn create(self, ctx: &Context) -> <Self as ComponentRpit<'_>>::Create;
}
With one significant difference being that the RPITIT is opaque -- it can never normalize, even if the implementor is known.
In a generic context like component
(where the implementor is not known), the above pattern can't normalize either, so the closure here
pub fn component<T: Component>(prop: T) {
move |ctx: &Context| prop.create(ctx);
}
has an intended signature like so
for<'a> fn(&'a Context) -> <T as ComponentRpit<'a>>::Create
and in this form we can see the "lifetime in the return type".
But it still seems like this should work. The error now looks like this:
error: lifetime may not live long enough
--> src/lib.rs:16:26
|
16 | move |ctx: &Context| prop.create(ctx);
| - - ^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is <T as ComponentRpit<'2>>::Create
| let's call the lifetime of this reference `'1`
Ah, with less elision, this is looking more familiar. It's Rust's notoriously horrible inference for "pass through a borrow" closures. When we're able to name things, we can fix this up:
pub fn component_with_funnel<T: Component>(prop: T) {
funnel::<T, _>(move |ctx: &Context| prop.create(ctx));
}
fn funnel<T: Component, F>(f: F) -> F
where
F: FnOnce(&Context) -> <T as ComponentRpit<'_>>::Create,
{ f }
Side note on the downside and unifying types
Here's the downside compared to just not capturing the lifetime: if the OP worked like I sketched out above, with the "intended" closure signature, it would mean the Context
stays borrowed as long as the return value is around, which you probably don't want. The signature you really want is something like
for<'a> fn(&'a Context) -> OneNonLifetimeAnnotatedType
So even if your OP compiled like it should, you may have found it to be annoying to use.
You can work around "too much capturing" with the ComponentRpit<'_>
pattern by unifying all the associated types with another type parameter. In that case the compiler realizes there's no way for the lifetime to be "captured". (component_with_unified_type
in the playground.) That's what you really want (which is analogous to not capturing the lifetime).
But there's no syntax for anything like that with RPIT. Even with return type notation, I'm not sure if it will be possible. You would need the ability to write a type equality bound against the return type. (I haven't kept up-to-date on the feature enough to know if that's a planned possibility or not.)
Unfortunately with the opaque RPITIT, we can't name things, and you're in the same conundrum as a lot of async
related closures and bounds -- no way to write the funnel (even if you were okay with the downsides).
Those are getting some async
-specific language additions to help soon, but since this isn't async
, you won't benefit. (I haven't had time to look into that feature and don't know why there isn't a non-async
counterpart.)
The typical workaround in async
today is to type erase everything. That allows you to name things again. In this case, the lifetime disappears altogether with type erasure, so you don't even need to deal with the borrowing downsides or need a funnel anymore:
pub fn component<T>(prop: T)
where
T: Component,
{
move |ctx: &Context| Box::new(prop.create(ctx)) as Box<dyn Any>;
}
(Playground.)
This lets you keep the opaque return type in the trait... but not in the closure.
I think there are number of developments that could help this use case and I don't know which are coming first (or at all).
- Precise capturing in traits
- Associated type
impl Trait
- Return type notation (could probably emulate associated type
impl Trait
or maybe unify types to emulate precise capturing)
- Better inference or annotations or
AsyncFn
like improvements for non-async
closures
impl Trait
in bounds ... could enable funnels for unnameable types
- RPIT for closures (with precise capturing?) ... could emulate annotations