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:
type Output;declares the placeholder. Inside the trait you refer to it asSelf::Output.- Each
implpicks the concrete type:type Output = i64;. After that, the impl's methods can writei64directly. - Generic code names it through the type parameter:
P::Outputis "whatever output this parser produces". You can also pin it in a bound:P: Parser<Output = i64>accepts only parsers that producei64, just likeI: Iterator<Item = u32>.
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
- Writing
Outputinstead ofSelf::Outputin the trait: the compiler sayscannot find type `Output` in this scope. Inside the trait it always belongs toSelf. - Forgetting
type Output = ...;in an impl:not all trait items implemented, missing: `Output`. - Adding a generic parameter "for flexibility": if each type would only ever implement it once, you've just made every caller write annotations.
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();.