We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Generics and Traits step 16 of 24
Associated type or generic parameter?
Fix Machine so it has one Parser implementation and two numeric Convert implementations. Keep the trait definitions and run signature unchanged.
pub trait Parser {
type Out;
fn parse_one(&self, raw: i64) -> Self::Out;
}
pub trait Convert<T> {
fn convert(&self, raw: i64) -> T;
}
For every input integer v, run must produce "<v>|byte|word":
Parser::OutisString, withparse_one(v)returningformat!("<{v}>").Convert<u8>returns the remainder modulo 256 as au8.Convert<u16>returns the remainder modulo 65,536 as au16.
Use rem_euclid so the remainder is nonnegative. For example, -1 produces "<-1>|255|65535", and 256 produces "<256>|0|256". Preserve input order and duplicates; empty input returns an empty vector. All i64 inputs are supported.
Why the two Parser implementations conflict
The starter implements Parser for Machine twice, choosing a different Out each time. Rust reports E0119. Changing an associated type does not make those two implementations distinct: both are still implementations of the same trait for Machine.
Keep the implementation with Out = String and remove the numeric Parser implementation. The numeric behavior belongs in Convert. The starter already implements Convert<u8>; add Convert<u16>.
These are two instantiations of the same generic trait with different type arguments. They can coexist because their implementation headers do not overlap. An associated type, by contrast, is selected within an implementation that has already been identified.
Type context can choose a conversion
After both conversions exist, let value = m.convert(-1); is ambiguous if nothing else determines the result type. A binding annotation or a qualified call can select the implementation:
// Fragment using the completed Machine implementations.
let m = Machine;
let byte: u8 = m.convert(-1);
let word = <Machine as Convert<u16>>::convert(&m, -1);
assert_eq!(byte, 255);
assert_eq!(word, 65535);
An annotation is not required at every call site. The surrounding expression may already provide the type. For example, this function's return type selects Convert<u16>:
// Fragment using the completed Machine implementations.
fn word(m: &Machine, value: i64) -> u16 {
m.convert(value)
}
Trait parameters are therefore not simply “types the caller must spell out.” They can be inferred from arguments, return types, or other constraints. The run function uses explicit annotations to make its two conversions easy to see.
Associated types belong to a particular implementation
For this non-generic Parser, Machine has one Out. More generally, a trait can have both generic parameters and associated types. Add<Rhs> is an example: the left-hand type and Rhs select an implementation, and that implementation defines Output.
use std::ops::Add;
struct Amount(i64);
impl Add<i64> for Amount {
type Output = Amount;
fn add(self, rhs: i64) -> Amount {
Amount(self.0 + rhs)
}
}
impl Add<&str> for Amount {
type Output = String;
fn add(self, rhs: &str) -> String {
format!("{} {rhs}", self.0)
}
}
fn main() {
assert_eq!((Amount(3) + 2_i64).0, 5);
assert_eq!(Amount(3) + "items", "3 items");
}
Here the same left-hand type has two different Output types because the Add arguments differ. TryFrom<T>::Error can likewise depend on both the implementing type and T. “One associated type per implementing type” would miss that distinction.
For Iterator::Item, the concrete iterator type determines the item type. Calling iter() on a Vec<i32> yields an iterator over &i32; consuming the vector with into_iter() yields a different iterator over i32. It is the iterator type, not just the collection's element type, that matters.
Trait-object syntax
The traits in this exercise can be used through trait objects. Specify the associated type for Parser and the generic argument for Convert:
// Fragment using the completed Machine implementations.
let parser: Box<dyn Parser<Out = String>> = Box::new(Machine);
let converter: Box<dyn Convert<u8>> = Box::new(Machine);
assert_eq!(parser.parse_one(7), "<7>");
assert_eq!(converter.convert(-1), 255);
Box<dyn Parser> omits the required Out binding and produces E0191. Writing Output instead of Out names an associated type this trait does not declare. Adding an unconstrained T to impl<T> Parser for Machine is not a way to obtain multiple implementations; it produces E0207.
Choosing a design
Use a trait parameter when distinct type arguments should identify distinct implementations, as with Convert<u8> and Convert<u16>. Use an associated type when the selected implementation should determine that type, as with Parser::Out. Both can appear in one trait. Consider the relationships your API needs before deciding which syntax makes calls shortest.
The remainders in this exercise fit their destination types: 0 through 255 for u8, and 0 through 65,535 for u16. A checked conversion can make that range assumption explicit. It cannot fail after these particular rem_euclid operations.
Further reading: associated types in the Rust Reference and Add's generic argument and associated output.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.