Closure return value lifetime propagation

Sorry newbie here (and in rust), and maybe not the best title. In the course of attempting to refactor some code, I ended into a trap of my own doing. I hope that someone can help me out of this (or tell me that there is no hope, using one of those nice 4 or 6 letter acronyms such as NONONO).

Here is the sample simplified issue:

use std::fmt::Debug;

#[derive(Debug)]
struct Foo<'a> {
    values: &'a [i32],
}

fn inner_call_elided_lifetime<F>(f: F)
where
    F: Fn(&[i32]) -> Foo,
{
    let values = [1, 2, 3];

    let foo = f(&values);
    println!("foo={:?}", foo);
}

fn inner_call_hidden_lifetime<F, D>(f: F)
where
    F: Fn(&[i32]) -> D,
    D: Debug,
{
    let values = [1, 2, 3];

    let foo = f(&values);
    println!("foo={:?}", foo);
}

fn outer_call() {
    inner_call_elided_lifetime(|vec| Foo { values: vec });
    inner_call_hidden_lifetime(|vec| Foo { values: vec });
}

The first call (inner_call_elided_lifetime) compiles fine, but rustc gives me the cold shoulder regarding the second. I cannot really complain, but his choice of wording is slightly awkward:

error: lifetime may not live long enough
  --> src/sandbox.rs:31:38
   |
31 |     inner_call_hidden_lifetime(|vec| Foo { values: vec });
   |                                 ---- ^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                                 |  |
   |                                 |  return type of closure is Foo<'2>
   |                                 has type `&'1 [i32]`

I would get it is rustc were saying the the thing cannot be inferred or something of that sort, but why inventing this Foo<'2> where the first call compiled smoothly?

Needless to say I tried to annotate with different variants of 'xyz the prototype of inner_call_hidden_lifetime in different manners to try to get this to compile and failed miserably. Is this even possible?

This is hiding a lifetime relationship. It is actually

    for<'a> F: Fn(&'a [i32]) -> Foo<'a>,

So, there is not one Foo type involved, but an entire infinite set Foo<'1>, Foo<'2>, .... You cannot represent that with a single type D.

1 Like

Thanks for the prompt reply. Yes, on the other hand, the elided_lifetime case seems to compile fine so rustc seems to assume that when i send &'a x the output is Foo<'x> and does not complain at all (maybe adding implicitly the statement you make).

Is there a way to tell it to do the same for the second case? In a way, there is also an infinite set of D that would match the second statement. If i understand correctly your statement, there is not way to constraint the relationship, is that what you are telling me?

You could write it like this (playground):

struct Foo1;

trait WithLifetime {
    type Type<'a>;
}

impl WithLifetime for Foo1 {
    type Type<'a> = Foo<'a>;
}

fn inner_call_hidden_lifetime<F, R>(f: F)
where
    R: WithLifetime,
    for<'a> F: Fn(&'a [i32]) -> R::Type<'a>,
    for <'a> R::Type<'a>: Debug,
{
   /* ... */
}

fn main() {
    inner_call_hidden_lifetime::<_, Foo1>(|vec| Foo { values: vec });
}

Ah, smart, introduce a trusted third party. I don't even really need to modify the original type Foo then (good news given the specifics of what i am working with). If I understand correctly, I can add this locally, where it is used: like a trill on a musical note held for a while.

I suppose it comes naturally after a while. Thanks!

May I know what is the difference between

fn inner_call_elided_lifetime<F>(f: F)
where
    for<'a> F: Fn(&'a [i32]) -> Foo<'a>,
{
    let values = [1, 2, 3];

    let foo = f(&values);
    println!("foo={:?}", foo);
}

and

fn inner_call_elided_lifetime<'a, F>(f: F)
where
    F: Fn(&'a [i32]) -> Foo<'a>,
{
    let values = [1, 2, 3];

    let foo = f(&values);
    println!("foo={:?}", foo);
}

the second one gives the following error

2 Likes

The second one:

fn inner_call_elided_lifetime<'a, F>(f: F)
where
    F: Fn(&'a [i32]) -> Foo<'a>,

Means "caller, provide me with a lifetime 'a which you can name, and a value of type F such that F is callable with &'a [i32] and returns a Foo<'a>, using that one particular lifetime 'a".

Callers cannot name lifetimes shorter than the duration of the function call. So you can never borrow a local for as long as a caller-provided lifetime -- such lifetimes that come from generic parameters on the function. That would be borrowing a local for a duration longer than the function body. Locals always drop or move by the end of the function body, so that's always a borrow check error.

That's why you can't pass the borrow of a local to f: f can only work with one particular lifetime, and it's a lifetime too long for borrowing a local.

In contrast, the first one:

fn inner_call_elided_lifetime<F>(f: F)
where
    for<'a> F: Fn(&'a [i32]) -> Foo<'a>,

means "caller, provide me a value of type F such that F is callable with &'a [i32] and returns a Foo<'a>, for any lifetime 'a".

In this case, every use of f inside the function body can choose the lifetime passed in to and returned from f. So you can pass in the borrow of a local to f.

The for<'a> ... is called a binder, and when it shows up in a trait bound like this, the bound is called a higher-ranked trait bound (HRTB). HRTBs are how you declare bounds when you need a bound involving lifetimes shorter than the function body -- lifetimes the caller cannot name.

3 Likes

:raised_hands: Thanks a lot. I learnt a new one today.

Nitpick: the two statements are true, but the latter is not due to the former. Even if the caller could name lifetimes shorter than the duration of the function call, it would still have the choice not to, in which case the function body would still be invalid. Take for example the trivial case where the caller substitutes 'static to 'a.

1 Like