Lifetime bounds with traits

So I stumbled upon this snippet where a manager (struct Library) manages something (struct Book) and lend them out (impl trait Reader).
Library itself records who had requested such services, say with a Vec<&mut impl Reader>.
trait Reader has methods that struct Library uses to determine if to lend books, and implementations probably keep some &Book in it, say Vec<&book>.

Here's 4 possible configurations and the same usage snippet. You might want to check out playground with nightly #[cfg(true)]/#[cfg(false)] toggles.

//! prob_a0

pub enum Proxy<'t, T> {
    None,
    Some(&'t mut T),
}

pub struct Library<'reader, R> {
    readers: Vec<Proxy<'reader, R>>,
}

impl<'reader, R: Reader<'reader> + 'reader> Library<'reader, R> {
    pub fn new(readers: impl IntoIterator<Item = &'reader mut R>) -> Self {
        Self {
            readers: Vec::from_iter(readers.into_iter().map(Proxy::Some)),
        }
    }
}
//! prob_a1

pub enum Proxy<'t, T> {
    None,
    Some(&'t mut T),
}

pub struct Library<'reader, R> {
    readers: Vec<Proxy<'reader, R>>,
}

impl<'book, 'reader, R: Reader<'book> + 'book> Library<'reader, R>
where
    'book: 'reader,
{
    pub fn new(readers: impl IntoIterator<Item = &'reader mut R>) -> Self {
        Self {
            readers: Vec::from_iter(readers.into_iter().map(Proxy::Some)),
        }
    }
}
//! prob_b0

pub struct Library<'reader, R>
where
    R: Reader<'reader>,
{
    readers: Vec<&'reader mut R>,
}

impl<'reader, R: Reader<'reader> + 'reader> Library<'reader, R> {
    pub fn new(readers: impl IntoIterator<Item = &'reader mut R>) -> Self {
        Self {
            readers: Vec::from_iter(readers),
        }
    }
}
//! prob_b1

pub struct Library<'reader, R>
where
    R: Reader<'reader>,
{
    readers: Vec<&'reader mut R>,
}

impl<'book, 'reader, R: Reader<'book> + 'book> Library<'reader, R>
where
    'book: 'reader,
{
    pub fn new(readers: impl IntoIterator<Item = &'reader mut R>) -> Self {
        Self {
            readers: Vec::from_iter(readers),
        }
    }
}
// all the problems may use this same snippet for testing
use std::iter::repeat_n;

#[derive(Debug)]
struct Book;

trait Reader<'reader> {
    fn borrow(&mut self, book: &'reader Book);
}

#[derive(Default, Debug)]
struct GenericReader<'reader> {
    books: Option<&'reader Book>,
}
impl<'reader> Reader<'reader> for GenericReader<'reader> {
    fn borrow(&mut self, book: &'reader Book) {
        self.books.replace(book);
    }
}

fn lifetime() {
    let books = vec![Book, Book, Book];
    let mut readers = Vec::from_iter(repeat_n((), 5).map(|_| GenericReader::default()));
    {
        let library = Library::new(&mut readers);
    }
    dbg!(&readers);
}

And they are basically the four configurations of

  1. Whether to add another generic lifetime parameter to the impl Library (0 vs 1)
  2. Whether if wrap the impl Reader with some enum (a vs b)

So the main question are these three:

  1. Why. Just, why. let library = Library::new( /* snip */ ) may once contained some exclusive references to some impl Reader, but the library is gone at the end of the local scope. No one is borrowing nothing at the dbg!. But 3 out of 4 configurations fails to compile. This just defies my generic PoV on Rust code.
  2. On the surface the prob_a1 is allowing the input impl Reader to have a different lifetime (larger) compared to that in prob_a0. And we know prob_a1 compiles while prob_a0 doesn't.
    • How does it work? How come there exists some pair of lifetimes satisfying prob_a1 but not a single standalone lifetime is possible for the prob_a0 configuration?
    • Is it related to the fact functions being contravariant, and I suppose Rust checks lifetime validity of all trait methods?
      • How does variance come into play exactly?
  3. prob_a1 and prob_b1 are similar, both trying to work around the lifetime bound, presumably by somehow relaxing the lifetime constraints of some sort by adding another generic lifetime parameter. But apparently prob_b1 fails to compile, while semantically it's (IMHO) basically the same as prob_a1: they both grant access to the &mut impl Reader if we have &mut Library. What's the deal here?

I'm so utterly confused. Please help shed some light.

Rust lifetimes are a type-level property which are generally about the duration of borrows, and they are not the liveness scope of values. Going out of scope can conflict with being borrowed, but the scope itself is not a lifetime. The braces around let library = ... do not change the borrow checking results in fn lifetime.

For a0, you end up with a Proxy<'reader, GenericReader<'reader>>. That contains a &'reader mut GenericReader<'reader>. That nesting of two identical lifetimes is a red flag, especially when the outer borrow is a &mut. You're borrowing something forever -- in this case, readers gets borrowed forever. You can only ever use it again via library.

The "why" is because of invariance. It's not the contravariance of functions, it's the invarariance of T in &mut T. You're creating an exclusive borrow of something else that contains a borrow 'x, and forcing that exclusive borrow to also be for 'x.

a1 allows the inner lifetime to be distinct from the outer lifetime and avoids the borrow-forever problem.

R: Reader<'book> doesn't imply R: Reader<'reader>, no matter the relationship between 'book and 'reader (other than equality). Trait parameters are invariant; trait bounds consider specific types and ignore variance; and similarly for implementations.

b1 also only fails due to this clause on the definition:

    pub struct Library<'reader, R>
    where
        R: Reader<'reader>, // <----
    {
        readers: Vec<&'reader mut R>,
    }

Remove that and it compiles and works for fn lifetime too.

2 Likes

Thx for the detailed reply and the awesome resource you've put together. Definitely gonna check them out later.

I'm still digesting so these might not be the right question to ask, but

Trait parameters are invariant

How does it work? I mean nomicon listed a table s.t. &'a mut T being covariant over 'a and invariant over T is somewhat straightforward, but what trait parameters are considered to be invariant? All of them, i.e. when we see trait MyTrait<'a, 'b, A, B>, and when we have impl<'a, 'b, A, B> MyTrait<'a, 'b, A, B> for MT, MT is considered to be invariant over all the lowercase lifetime and uppercase type parameters? If so, I guess it's being generic over every possible trait implementations means we must forbid any possible misuse, requiring exactly the same type rather than allowing for any subtype/supertype relation, no?

And...

trait bounds consider specific types and ignore variance; and similarly for implementations.

Is it saying the trait bound clauses i.e. those following : or where are not related to the variance type system? Probably because trait parameters are invariant anyway?

For types: given a dyn MyTrait<'a, 'b, A, B> + 'd, all of 'a, 'b, A, and B are invariant, and 'd is covariant.


For bounds, where T: MyTrait<'a, 'b, A, B> never allows you to assume T: MyTrait<'a2, 'b, A, B> unless 'a2 equals 'a exactly, and similarly for the other parameters.


For implementations:

impl<'a, 'b, A, B> MyTrait<'a, 'b, A, B> for MyType<'a, 'b, A, B>

Never means

impl<'a2, 'a, 'b, A, B> MyTrait<'a2, 'b, A, B> for MyType<'a, 'b, A, B>
where
    'a2: 'a

Or

impl<'a2, 'a, 'b, A, B> MyTrait<'a2, 'b, A, B> for MyType<'a, 'b, A, B>
where
    'a: 'a2

Or any other variance-related generalization; MyType<'a, 'b, A, B> implements only MyTrait<'a, 'b, A, B> exactly.


So in general, when you see a parameterized trait, the parameters are exact: they don't coerce to something else or imply anything about different parameter "values", and so on.

In the original b1, Library<'reader, R> required R: Reader<'reader> (exactly), but in the implementation R: Reader<'book> (only), which is why it gave a compilation error.


There are also higher-ranked types and trait bounds,[1] which can imply multiple parameter "values".


  1. for<'a> ... ↩︎

1 Like

Wow this is a little... counterintuitive? I mean one may see at first glance that yeah it's MyType<'a, 'b, A, B> being equipped with some extra functionalities in this impl block, but the fact specifying MyTrait<'a2, 'b, A, B> doesn't change the fact it's indeed MyTrait<'a, 'b, A, B> being implemented, whether 'a: 'a2 or 'a2: 'a, is a bit convoluting to me.

One may say it's what it is, but I'm wondering what are the considerations behind this language feature? Or maybe insights/examples that illustrate why it should behave as such? Is it because

given a dyn MyTrait<'a, 'b, A, B> + 'd, all of 'a, 'b, A, and B are invariant, and 'd is covariant.

since they are invariant and thus always require exact type match, s.t. it's of little use allowing MyTrait<'a2, 'b, A, B> to be implemented for MyType<'a, 'b, A, B>?

Well, that's subjective. I have no idea what percentage of programmers find it intuitive or unintuitive.

Traits had variance before 1.0 for at least a period, but it was decided to ditch it. According to the dev guide:

One complication was that variance for associated types is less obvious, since they can be projected out and put to myriad uses, so it's not clear when it is safe to allow X<A>::Bar to vary (or indeed just what that means). Moreover (as covered below) all inputs on any trait with an associated type had to be invariant, limiting the applicability. Finally, the annotations (MarkerTrait, PhantomFn) needed to ensure that all trait type parameters had a variance were confusing and annoying for little benefit.

(N.b. the rest of that addendum is quite confusing for a myriad of reasons.[1])

I think it's all part of the same package. If you had variance on trait parameters, an implementation could be more general than it looks, dyn Trait<P> types could coerce to something with a different parameter, etc.


  1. Written before dyn, written from a Java perspective, examples that make no sense in Rust... ↩︎

1 Like

I don't think this is what they meant.

If you write:

impl<'a, 'a2, 'b, A, B> MyTrait<'a2, 'b, A, B> for MyType,'a, 'b, A, B>

then MyType<'a, 'b, A, B> will implement MyTrait<'a2, 'b, A, B> for any 'a2, while also for exactly the same 'b, A and B as in the type.

1 Like

Oh so I misunderstood. Thx for pointing it out.

The counterintuitive part was kindly pointed out by SkiFire13: I misinterpreted the example around impl<'a, 'a2, 'b, A, B> MyTrait<'a2, 'b, A, B> for MyType,'a, 'b, A, B>.

I think all the dots are there, albeit I still need some time connecting them. Thx again for your kindness and the compilation of resources you've put together.

1 Like