While functions operate on variables, you can think of traits as functions that operate on types. When thought of this way, the type parameters serve as inputs to the function and associated types serve as outputs.
Since Output is an associated type, for two types A and B that we wish to impl Add, we are restricted to picking a single Output type. Were Output a type parameter, we could impl Add for A and B in a myriad of ways.
For example, let us define a Mul trait where Output is a parameter:
trait Mul<RHS, Output> {
fn mul(self, rhs: RHS) -> Output;
}
Now let us define a Complex type:
#[derive(Debug, Clone, Copy)]
struct Complex {
x: f64,
y: f64,
}
impl Complex {
fn new(x: f64, y: f64) -> Complex {
Complex { x: x, y: y }
}
}
We want to be able to multiply it by f64:
impl Mul<f64, Complex> for Complex {
fn mul(self, rhs: f64) -> Complex {
Complex::new(self.x * rhs, self.y * rhs)
}
}
This all works fine. However, we could come up with a second implementation:
impl Mul<f64, f64> for Complex {
fn mul(self, rhs: f64) -> f64 {
self.x * rhs
}
}
When we multiply a Complex by a f64 now, it is ambiguous which implementation should be called, and extra type information is needed to be supplied by the caller. By making Output an associated type, this is disallowed. The following code throws a compiler error for conflicting implementations:
impl std::ops::Mul<f64> for Complex {
type Output = Complex;
fn mul(self, rhs: f64) -> Complex {
Complex::new(self.x * rhs, self.y * rhs)
}
}
impl std::ops::Mul<f64> for Complex {
type Output = f64;
fn mul(self, rhs: f64) -> Complex {
self.x * rhs
}
}
Full example