The lifetime problem of `chain` two closures in one

I am confused why this snippet can't compile

fn chian<T, S, R>(
  f1: impl Fn(&T) -> &S, 
  f2: impl Fn(&S) -> &R
) -> impl Fn(&T) -> &R {
  move |v| f2(f1(v))

compile error:

error[E0310]: the parameter type `S` may not live long enough
  --> core/src/state/
64 |   move |v| f2(f1(v))
   |            ^^^^^^^^^ that the type `S` will meet its required lifetime bounds
help: consider adding an explicit lifetime bound...
63 | fn chian<T, S: 'static, R>(f1: impl Fn(&T) -> &S, f2: impl Fn(&S) -> &R) -> impl Fn(&T) -> &R {

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.[1]

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 :slight_smile:

  1. We are able to come very close to working around this limitation (playground) but that hits an error message that iterally quotes “current limitations in the borrow checker:laughing: ↩︎


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.