RustArena

Step 1 of 7

Associated types versus generic parameters

You've used an associated type since the iterators module: Iterator declares type Item;, and every iterator says what its Item is. An associated type is a placeholder type inside a trait that each implementation fills in exactly once.

trait Parser {
    type Output;
    fn parse(&self, input: &str) -> Option<Self::Output>;
}

struct IntParser;
struct PairParser;

impl Parser for IntParser {
    type Output = i64;
    fn parse(&self, input: &str) -> Option<i64> {
        input.trim().parse().ok()
    }
}

impl Parser for PairParser {
    type Output = (i32, i32);
    fn parse(&self, input: &str) -> Option<(i32, i32)> {
        let (a, b) = input.split_once(',')?;
        Some((a.trim().parse().ok()?, b.trim().parse().ok()?))
    }
}

fn parse_all<P: Parser>(parser: &P, lines: &[&str]) -> Vec<P::Output> {
    lines.iter().filter_map(|line| parser.parse(line)).collect()
}

fn main() {
    println!("{:?}", parse_all(&IntParser, &["10", "x", " 32 "]));
    println!("{:?}", parse_all(&PairParser, &["1,2", "3;4", "5, 6"]));
}
[10, 32]
[(1, 2), (5, 6)]

Piece by piece:

Why not a generic parameter?

You could write the trait as trait Parser<T> instead. The difference is how many implementations a type may have. With a generic parameter, one type can implement the trait many times, once per T. That's sometimes exactly right, but it means the compiler can't guess which one you mean:

trait ParseAs<T> {
    fn parse_as(&self) -> Option<T>;
}

impl ParseAs<i32> for str {
    fn parse_as(&self) -> Option<i32> {
        self.parse().ok()
    }
}

impl ParseAs<bool> for str {
    fn parse_as(&self) -> Option<bool> {
        self.parse().ok()
    }
}

fn main() {
    let n = "42".parse_as();
    println!("{:?}", n);
}
error[E0283]: type annotations needed for `Option<_>`

Writing let n: Option<i32> = "42".parse_as(); fixes it, but every caller now has to annotate. With an associated type there's one impl per type, so the type alone tells the compiler the answer.

Pick When Examples in std
associated type each implementing type has one natural answer Iterator::Item, Deref::Target, FromStr::Err
generic parameter a type should implement the trait several times, for different types From<T>, AsRef<T>, PartialEq<Rhs>

An iterator over bytes yields one kind of item (u8), so Item is associated. A String can be built From<&str>, From<char> and From<Box<str>>, so From<T> is generic. Some traits use both, as you'll see in the next step.

Common mistakes

Your task

The Sensor trait has an associated type Reading. Implement it for Thermometer (Reading is f64; the raw value is in tenths of a degree, so 215 reads as 21.5) and for DoorSwitch (Reading is bool; any raw value other than 0 means open, true). Then write the generic read_all, which reads every raw value with the given sensor and returns the readings in order.

Challenge 2

Fill in the blanks: declare the associated type in the trait, give Doubler its output type, and use the associated type in the generic function's return type. The program should print (8, 42) and then ("#007", "#042").

Challenge 3

This time a generic parameter is the right tool: a distance in meters can sensibly be converted into several other units. Write a generic trait ConvertTo<T> with one method fn convert(&self) -> T;, and implement it twice for Meters: ConvertTo<Feet> (1 m = 3.28084 ft) and ConvertTo<Centimeters> (1 m = 100 cm). Callers pick the impl by the type they ask for: let f: Feet = Meters(2.0).convert();.

Next: Operator traits and default type parameters