Why do associated types in rust need explicit lifetime annotation?

Hello everyone! I'm working with a trait which cannot be touched like this(minimized):

// The associated version
trait Testable {
    type T;
    fn test_it(&self, x: Self::T) -> bool;
}

Then I try to impl it with, say, i32:

impl Testable for i32 {
    type T = &str;
    fn test_it(&self, x: Self::T) -> bool {
        x.is_empty()
    }
}

However, I got compiler error:

type T = &str;
         ^ explicit lifetime name needed here

The associated type Self::T is just on the input parameter of method test_it. Why does the compiler claim that I have to provide lifetime annotation?

Note that if I change T to generic type like:

// The generic version
trait Testable<T> {
    fn test_it(&self, x: T) -> bool;
}

impl Testable<&str> for i32 {
    fn test_it(&self, x: &str) -> bool {
        x.is_empty()
    }
}

This time the code compiled without error.

The problem is

(1) why I have to provide lifetime annotation in the associated version since the type only appears at the input parameter side?

(2) Why the generic version compiled? Are there any deep differences between the two versions?

Yes. The associated type lets you write code like this which is impossible with the generic version:

struct TestInstance<X:Testable> {
    to_test: X,
    comapare_to: X::T,
}

This works because there can only be one implementation of Testable for any given type, and so X::T is fully determined for any given type X. This, in turn, means that the associated type T needs to be a fully-specified type; for references, that include a lifetime annotation.

3 Likes

It's for the same reason that you can't do this:

impl Testable for i32 {
    type T = String;
    fn test_it(&self, x: Self::T) -> bool {
        x.is_empty()
    }
}

impl Testable for i32 {
    type T = Vec<u8>;
    fn test_it(&self, x: Self::T) -> bool {
        x.is_empty()
    }
}

but you can do this:

impl Testable<String> for i32 {
    fn test_it(&self, x: String) -> bool {
        x.is_empty()
    }
}

impl Testable<Vec<u8>> for i32 {
    fn test_it(&self, x: Vec<u8>) -> bool {
        x.is_empty()
    }
}

That is, it is because <i32 as Testable>::T must unambiguously be a single specific type.

To be clear, &'a str and &'b str are different types when 'a ≠ 'b, in the same way that Vec<u8> and Vec<String> are also different types.

4 Likes

Consider what would happen if the lifetime were unknown. How is the compiler supposed to know what lifetime parameter it should substitute? Since the associated type in itself isn't constrained in any way, there is no way for the compiler to infer the lifetime in the way it could if the type were actually used in a piece of code.

For example, what is the following piece of code supposed to mean?

trait GetValue {
    type T;
    fn get_value(&self) -> T;
}

struct Foo {
    string: String
}


impl GetValue for Foo {
    type T = &str;
    fn get_value(&self) -> Self::T {
        &self.string
    }
}

fn main() {
    let string: &'static str = Foo.get_value();
}

If there are no lifetime annotations, then how is the compiler supposed to know that the code above is invalid? There's nothing that would tell it that the returned string reference is tied to the lifetime of self or that it's otherwise not valid for the 'static lifetime.

Your confusion might be that you think that Rust generics are like C macros or C++ templates. They aren't. When you write an associated type, the compiler doesn't, can't, simply copy-paste its definition syntactically at the use site. (That would be disastrous, because it would change the meaning of the trait based on the interaction of lifetime elision and the concrete trait definition/impl for each specific type). Thus, the following declarations aren't equivalent:

impl Foo {
    // desugars to: fn get_value<'a>(&'a self) -> &'a str
    fn get_value(&self) -> &str { ... }
}

impl GetValue for Foo {
    type T = &'b str; // NB: 'b has to come from somewhere, too!

    // desugars to fn get_value<'a>(&'a self) -> &'b str;
    fn get_value(&self) -> Self::T { ... }
}
1 Like

Thanks! Now I understand the reason for associated type design. I like your explanation.

1 Like

Yes I used to view rust's polymorphism as old day's C++ templates. Thanks!

Interesting example code! Thanks!