Skip to content

← Generics and Traits step 16 of 24

Hard Primitives

Associated type or generic parameter?

This is the most consequential API-design decision in Rust traits, and the one most often got wrong. The two shapes look almost identical:

pub trait Parser  { type Out; fn parse_one(&self, raw: i64) -> Self::Out; }
pub trait Convert<T> {        fn convert(&self, raw: i64) -> T; }

The difference is one sentence:

An associated type is an output determined by the impl — a type may implement the trait once. A generic parameter is an input chosen by the caller — a type may implement the trait many times.

That is why Iterator::Item is an associated type (a Vec<i32>‘s iterator yields i32 and nothing else, ever) while From<T> is generic (a String can be built from a &str, from a char, from a Box<str> — many impls, one type).

Get this wrong and you either lock users out of a legitimate second impl, or you force every single call site to carry a type annotation forever.

Your task

pub struct Machine;

impl Parser for Machine   { type Out = String; ... }
impl Convert<u8>  for Machine { ... }
impl Convert<u16> for Machine { ... }

pub fn run(vals: Vec<i64>) -> Vec<String>

For each input value produce "<v>|byte|word", where

  • <v> comes from parse_oneformat!("<{raw}>")
  • byte is raw.rem_euclid(256) as a u8
  • word is raw.rem_euclid(65_536) as a u16

rem_euclid rather than % so negatives wrap the way you would expect: -1 becomes 255 and 65535, not -1.

The error you must hit

The starter contains two impl Parser for Machine blocks, one with type Out = String and one with type Out = u8. Compile it:

error[E0119]: conflicting implementations of trait `Parser` for type `Machine`

This is the rule made concrete. The associated type is not part of the trait’s identity — Parser is Parser, whatever Out you pick — so two impls collide the same way two impl Display for Money blocks would. Delete the second one.

Then look at what Convert does instead. Convert<u8> and Convert<u16> are different traits as far as coherence is concerned, so both impls are legal and both coexist happily on Machine.

The cost of choosing generic

Nothing is free. Because Machine implements Convert twice, this does not compile:

let x = m.convert(v);       // which one?

You must disambiguate, either by annotating the binding or with a turbofish:

let byte: u8 = m.convert(v);
let byte = Convert::<u8>::convert(&m, v);

The starter’s run already annotates both bindings, and that is exactly the tax a generic parameter imposes on every caller forever. Compare parse_one, which needs no annotation at all because the impl already decided.

That trade-off is the decision. Ask: could a sensible user want two different answers here? If yes, generic parameter and accept the annotations. If no, associated type and give callers inference.

Three more errors in the same family

  • E0191: Box<dyn Parser> is rejected — “the value of the associated type Out must be specified”. Associated types must be named in a trait object: Box<dyn Parser<Out = String>>. Generic parameters must be supplied: Box<dyn Convert<u8>>. Different syntax, different error, same root cause — a trait object needs one concrete shape.
  • E0207: try moving the parameter onto the impl instead of the trait — impl<T> Parser for Machine — and you get “the type parameter T is not constrained by the impl trait, self type, or predicates”. Nothing determines T, so there would be infinitely many impls.
  • E0220 is what you get for naming an associated type that does not exist, e.g. dyn Parser<Output = String>.

Where you have already seen both

  • Iterator::Item, Deref::Target, Add::Output, TryFrom::Error — associated types. One answer per implementing type.
  • From<T>, PartialEq<Rhs>, Index<Idx>, Convert<T> here — generic parameters. String: PartialEq<str> and String: PartialEq<&str> are both real, which is exactly why that one is not associated.

Add<Rhs = Self> shows the hybrid: a generic parameter with a default, so the common case reads like there is no parameter at all.

Remember the grade is compile + tests + clippy -D warnings.