This is due to how HRTBs work.
The bound Fn(&T) -> &S
stands for for<'a> Fn(&'a T) -> &'a S
, which in turn stands for something there isn’t quite syntax for, but let’s assume we can add a where clause, then it stands for something like
for<'a> [where T: 'a, S: 'a] Fn(&'a T) -> &'a S
i.e. it requires Fn(&'a T) -> &'a S
precisely for those lifetimes such that T: 'a
and S: 'a
are true.
The same of course is true for the other two trait bounds.
So Fn(&T) -> &S
, Fn(&S) -> &R
and Fn(&T) -> &R
stand for
for<'a> [where T: 'a, S: 'a] Fn(&'a T) -> &'a S
for<'a> [where S: 'a, R: 'a] Fn(&'a S) -> &'a R
for<'a> [where T: 'a, R: 'a] Fn(&'a T) -> &'a R
notably the where
clauses are different.
Assume we want to verify for any given lifetime 'a
matching the where clause where T: 'a, R: 'a
, that Fn(&'a T) -> &'a R
is truly implemented by our return value. For the closure to work correctly, both f1
and f2
must thus be able to, one after the other, process &'a T
. Buf for f1
already, the call faces a problem: The where
clause for it requires T: 'a
which we have assumed, and S: 'a
, which is a relation we don’t know is true.
The compiler ultimately infers that fabricating the necessary S: 'a
bound would only really be possible if the function also requires S: 'static
(which does imply S: 'a
for all lifetimes 'a
).
This may seem a bit overly restrictive… more precisely we’d only need to require S: 'a
for those lifetimes that fulfill T: 'a
and R: 'a
, but Rust offers no syntax to write such a relationship either.
Regarding solutions, thus adding S: 'static
is a reasonable way to address the issue.
Other reasonable ways:
Perhaps you never intended to have higher-ranked types in the first place. You could write a function
fn chain<'a, T: 'a, S: 'a, R: 'a>(
f1: impl Fn(&'a T) -> &'a S,
f2: impl Fn(&'a S) -> &'a R,
) -> impl Fn(&'a T) -> &'a R {
move |v| f2(f1(v))
}
that only involves non-higher-ranked types, though arguably in that case, you might as well rewrite it into the more general
fn chain<T, S, R>(
f1: impl Fn(T) -> S,
f2: impl Fn(S) -> R,
) -> impl Fn(T) -> R {
move |v| f2(f1(v))
}
Or perhaps you might desire to add something akin to a modified where
bound on the higher-ranked type… really if we could specify that the returned closure still requires S: 'a
…
… but we can only somewhat clunkily and indirectly do that: By adding a PhantomData<&'a S>
argument that’s otherwise going to be irrelevant/ignored
use core::marker::PhantomData;
fn chain<T, S, R>(
f1: impl Fn(&T) -> &S,
f2: impl Fn(&S) -> &R,
) -> impl for<'a> Fn(&'a T, PhantomData<&'a S>) -> &'a R {
move |v, _| f2(f1(v))
}
What solution may be best depends strongly on your use-case for this function, and arguably no approach is really perfect 