Skip to main content
Megh's Blog

Rust Traits - Associated Types and Generic Traits

Rust's traits have a nifty feature called Associated Types. Rust's traits can also have Generic Type Parameters.

This post delves into some differences between the two, whether both are needed and my mental model of these concepts.

I am not a Rust pro, and came across this while relearning traits. Please let me know your comments!

Are both features needed? #

The Rust book itself asks this question:

Associated types might seem like a similar concept to generics, in that the latter allow us to define a function without specifying what types it can handle. To examine the difference between the two concepts, we’ll look at an implementation of the Iterator trait on a type named Counter that specifies the Item type is u32:

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<Self::Item> {}
}

This syntax seems comparable to that of generics. So why not just define the Iterator trait with generics, as shown in Listing 20-14?

pub trait Iterator<T> {
   fn next(&mut self) -> Option<T>;
}

(From the Rust Book)

It further states that

The difference is that when using generics, as in Listing 20-14, we must annotate the types in each implementation; because we can also implement Iterator for Counter or any other type, we could have multiple implementations of Iterator for Counter. In other words, when a trait has a generic parameter, it can be implemented for a type multiple times, changing the concrete types of the generic type parameters each time. When we use the next method on Counter, we would have to provide type annotations to indicate which implementation of Iterator we want to use.

(From the Rust Book)

Trait inferring - works even for generic traits #

Part of the above statement is is not true. The following works without explicit qualification of the trait:

trait MyTrait<T> {
    fn first_element(&self) -> Option<T>;
}
impl MyTrait<i64> for Vec<i64> {
    fn first_element(&self) -> Option<i64> { None }
}
fn main() {
    let v = vec![1, 2, 3];
    println!("{:?}", v.first_element());
}

Thus if you have a generic type, and only one implementation of it, Rust is able to do a name-based lookup for it. This is similar to how it does name-based lookup for traits in the first place, i.e. on noticing v.first_element(), it:

  1. Analyzes the type of the object the method is being called on, i.e. v -> Vec<i64>
  2. List traits implemented for it, i.e. Index, MyTrait and so on
  3. See if any of them have the matching method call, i.e. first_element()
  4. If there are multiple matches, error out with an ambiguous match error

Uniqueness constraint #

However, with associated types, the useful contract that a trait can only be implemented once for a type remains. I.e, the following is possible with generic traits but not with associated types.

impl MyTrait<i64> for Vec<i64> {
    fn first_element(&self) -> Option<i64> { None }
}
impl MyTrait<u64> for Vec<i64> {
    fn first_element(&self) -> Option<u64> { None }
}
// Need to disambiguate the trait to be called
println!("{:?}", MyTrait::<i64>::first_element(&v));

So, are there other differences?

More differences - returning trait objects #

Let's assume we want to define vec_iter which wraps vec.iter() such that we can

fn main() {
    let v = vec![1, 2, 3];
    for x in vec_iter(&v) {
         println!("{}", x);
    }
}

In the case of associated types, we can specify the trait without specifying the associated type.

// Valid to only specify Iterator without its associated type Item
fn vec_iter(x: &Vec<i64>) -> impl Iterator + '_ { x.iter() }
// Valid to call this
let _ = vec_iter(v);

This itself is not very useful in most cases, as using it would have caused an error

fn vec_iter(x: &Vec<i64>) -> impl Iterator + '_ { x.iter() }

// Invalid as type of x is "opaque"
for x in vec_iter(&v) {
    println!("{}", x);
    // ^ error[E0277]: `<impl Iterator as Iterator>::Item` doesn't implement `std::fmt::Display`
}

Returning a dyn of a trait is invalid when associated types are not specified

fn vec_iter(x: &Vec<i64>) -> dyn Iterator { x.iter() }
// error[E0191]: the value of the associated type `Item` (from trait `Iterator`) must be specified

Returning a generic trait is also (understandably) invalid

fn vec_iter(x: &Vec<i64>) -> impl MyTrait { x.first_element() }
    // ^ error[E0107]: missing generics for trait `MyTrait`

Thus even in the above case, specifying the associated type is necessary in most cases.

More differences - multiple associated types #

In the case of atrait having multiple associated types, it is valid to associate only one of them and use it.

trait MyTrait {
    type A;
    type B;

    fn double_a(&self, a: Self::A) -> Self::A;
    fn double_b(&self, b: Self::B) -> Self::B;
}
fn example_fn() -> impl MyTrait<A = u32> { ExampleStruct{} }

fn main() {
    let x = example_fn();
    println!("{}", x.double_a(2));
    println!("{}", x.double_b(2.0));
    // error[E0277]: `<impl MyTrait<A = u32> as MyTrait>::B` doesn't implement `std::fmt::Display`
}

This does not seem as useful as traits are intended to be "small / composable", so if you have multiple associated types, you are likely using all of them or none of them.

My mental model #

My unpolished syntax alternative

What about something like?

trait Iterator<Item> {
	// no type statement, it becomes a generic argument consistenly
	require unique implementation for <Item>
}

impl<T> Iterator<Item=T> for Vector<T> { ... }

trait ToPairs<Item1, Item2> {
	require unique implementation for <Item1, Item2>
}

trait NDShape<BaseUnit, PerimeterUnit, AreaUnit> {
	// PerimeterUnit & AreaUnit are associated types, but BaseUnit is not
	require unique implementation for <BaseUnit>
}

(I admit its not great though)

Aside: In nightly, there is #![feature(associated_type_defaults)] which affects trait implementors, not users, by providing defaults for associated types.