Intuition for lifetime constraints

I have the following program (playground). I can get it to work, but I don't get the intuition behind the working versions.

Note that all the ...X types are simplified version from the graphql-parser library.

pub trait TextX<'a> {
    type Value; // Originally specified as `type Value: 'a`, but doesn't seem to matter
}

impl<'a> TextX<'a> for String {
    type Value = String;
}

pub struct ValueX<'a, T: TextX<'a>> {
    var: T::Value
}

pub struct AppContext<'ac> {
    data: &'ac str
}
struct LocalContext<'lc> {
    app_context: &'lc AppContext<'lc>
}

pub fn outer<'a>(value: &'a ValueX<'a, String>, app_context: &'a AppContext<'a>) {
    let local_context = LocalContext { app_context };
    inner(value, &local_context);
}

fn inner<'a>(_value: &'a ValueX<'a, String>, _local_context: &'a LocalContext<'a>) {
    todo!()
}

// Works: Different lifetimes for each parameter
// fn inner<'a, 'lc>(_value: &'a ValueX<'a, String>, _local_context: &'lc LocalContext<'lc>) {
//     todo!()
// }

// Works: The same as above, I think, but with inferred lifetimes
// fn inner(_value: &ValueX<String>, _local_context: &LocalContext) {
//     todo!()
// }

// Works: Separate lifetimes for the LocalContext reference and content
// 'a: 'lc isn't really needed (i.e. just `'a` suffices)
// fn inner<'a: 'lc, 'lc>(_value: &'a ValueX<'a, String>, _local_context: &'lc LocalContext<'a>) {
//     todo!()
// }

The way I am thinking:

  • In the outer function, the lifetime of value is longer than that of local_context (content of local_context live longer, but not sure if that matters).
  • The (non-working) inner function expects both parameters (as well as the structure content) to have the same lifetime.
  • Using the shorter lifetime (that of local_context) would have satisfied the lifetimes constraint of the inner function.

However, to get inner to compile I need one of the variations shown.

Also, I need the structure of TextX (specifically, it needs a lifetime parameter--here unused, but the original library code needs it) to illustrate the issue. If I remove the lifetime on TextX and propagate that change to other types, the code works as well.

What may be going on here and where is my thinking going wrong?

Thanks.

One quick comment that may be helpful. You almost never want this. This construct states that the outer lifetime of the reference must be exactly the same as the inner lifetime of LocalContext. Generally speaking you want to allow the outer lifetime to be shorter than the inner lifetime so something like

&’may_be_shorter_than_a LocalContext<‘a>

You have this in several places in that example and it certainly leads to the difficulties. I’m on my phone so it’s a bit tough to give a more complete answer. I’ll respond more tomorrow if no one else comes in with a longer answer.

3 Likes

I see nothing called field so I can't really comment on this part.

Given a ValueX<'lifetime, T>, T must implement TextX<'lifetime>. And trait implementations are invariant over their lifetimes. This makes the 'lifetime in ValueX<'lifetime, T> invariant as well.

This means it can't be coerced into a shorter lifetime.

Because all the lifetimes on the inner() declaration are the same, this invariant lifetime gets forced onto all the other lifetimes -- including the reference to the local_context. This would force the reference to last longer than the local_context in the outer() function, and hence the error.

If you loosen up the bounds on the inner() function, the &local_context reference can have a shorter lifetime, and it compiles. If you remove the lifetime on the trait, the invariance goes away, and the lifetime of everything can be coerced to something shorter when you call inner(), which also compiles. (If I understand correctly, this latter scenario is what you expected to happen.)

Edit:: Here's a good StackOverflow post on the topic, and a playground example of the associated type case like that in your code.

As @drewkett said, you don't want to over-constrain your lifetime bounds. Here's a version with much less bounds.

1 Like

I meant value (forgot to sync up that as I was trying to make snippet easier to understand). I have edited the post now. Oops!

This is the key aspect that I did not realize. This all makes sense now. Thanks!

You are right. In fact, in my real code, I do need lifetime for the reference to be separate that its content (due to other concerns such as returning a part of that content with a correct lifetime). But I have encountered such scenario a few times and was able to get away with one of the variations shown later in the snippets, without developing a good intuition about why I need to do so. Answers here helped me towards that. Thanks.

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.