Associated type in trait bounded by Iterator

Hi, pretty new to Rust and thought I was getting a hang of the type system until I tried this:

trait MyTrait {
    type FirstType;
    type SecondType;
    type IteratorType: Iterator<Item=(&Self::FirstType, &Self::SecondType)>
}

This doesn't compile because of something to do with lifetimes. My understanding of lifetime annotations is that they are a way to create death pacts between references. So I figured I want the two references returned by the iterator to be valid for the same time and tried my second approach:

trait MyTrait<'a> {
    type FirstType;
    type SecondType;
    type IteratorType: Iterator<Item = (&'a Self::FirstType, &'a Self::SecondType)>
}

The compiler went nuts after this saying something about:

The associated type may not live long enough, consider adding an explicity lifetime bound

I ended up just copying the snippets the error gave me and ended up with this error instead

trait MyTrait<'a> {
    type FirstType;
    type SecondType;
    type IteratorType: Iterator<Item = (&'a <Self as MyTrait<'a>>::FirstType: 'a, &'a <Self as MyTrait<'a>>::SecondType: 'a)>
}

associated const equality is incomplete

What is going on here?

Lifetime syntax is not about throwing 'as everywhere. A lifetime has a meaning; in this case, because you are using the same lifetime for everything you are telling the compiler that the lifetime of the reference returned by the iteration is equal to the lifetime of the iterator itself. This is obviously not what you meant to tell the compiler.

The reference must not live longer than its referent. Otherwise you would have a use-after-free problem. In your case every implementor of MyTrait must make sure that Self::FirstType (or any references it might contain) lives longer than 'a (because the iterator will create &'a references to it). (And similarly for SecondType of course.)

To express such constraint you can use the following bounds (this will compile):

trait MyTrait<'a> {
    type FirstType: 'a;
    type SecondType: 'a;
    type IteratorType: Iterator<Item = (&'a Self::FirstType, &'a Self::SecondType)>;
}
2 Likes

Well, you can't introduce lifetimes in trait bounds out of nowhere. [1] I'm going to guess that you wanted something that eventually looks something like

trait MyTrait {
    // [types]
    fn f(&self /* , other args */) -> Self::IteratorType;
}

where the iterator returned from f is borrowing from self. In that case, you need a method signature that conveys that whatever the lifetime on the &self reference is, you can create an iterator over items with the same lifetime -- you need a lifetime connection between &self and the iterator. There is none in the code block above.

To introduce such a connection, you could have something like:

trait MyTrait {
    type FirstType;
    type SecondType;
    //               vvvv  (lifetime cnxn)   vv                   vv
    type IteratorType<'a>: Iterator<Item = (&'a Self::FirstType, &'a Self::SecondType)>
    where
        Self: 'a,
        Self::FirstType: 'a, 
        Self::SecondType: 'a;
    //   v  (lifetime cnxn via lifetime elision)        vvvv
    fn f(&self /* , other args */) -> Self::IteratorType<'_>;
}

In order to show that the IteratorType has a lifetime dependent on &self, it needs to be parameterized by a lifetime -- it needs to be a generic associated type (GAT), generic over the lifetime.

The lifetime elision is the same as

    //        vv  (lifetime cnxn)                              vvvv
    fn f<'s>(&'s self /* , other args */) -> Self::IteratorType<'s>;

A pre-GAT alternative[2] would be something like

trait MyTraitBasis<'a /*, _ImplicitBound = &'a Self */> {
    type FirstType: 'a;  // <-- (x)
    type SecondType: 'a; // <-- (x)
    type IteratorType: Iterator<Item = (&'a Self::FirstType, &'a Self::SecondType)>;
    fn f_basis(&'a self) -> Self::IteratorType;
}

trait MyTrait: for<'any> MyTraitBasis<'any> {
    fn f(&self) -> <Self as MyTraitBasis<'_>>::IteratorType;
}

// Maybe with further bounds so the other associated types are
// the same regardless of the lifetime
impl<T: for<'any> MyTraitBasis<'any>> MyTrait for T {
    fn f(&self) -> <Self as MyTraitBasis<'_>>::IteratorType {
        self.f_basis()
    }
}

The annotations marked with (x) are what the compiler was trying to suggest with your second code sample.


If my assumptions about what you were trying to do with the trait were off, please share more information, such as the signatures of the methods of the trait. (E.g. it's possible you were trying to definite a lending iterator trait, which is incompatible with the Iterator trait.)


  1. If you think you can because you've seen something like <F: FnMut(&str)>, that's completely understandable; say so and I'll write up an explanation. ↩︎

  2. sometimes still necessary for various compiler limitations which hopefully go away some day ↩︎

4 Likes

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.