Skip to content

← Generics and Traits step 15 of 24

Medium Primitives

Operator overloading: + is just a trait method

Rust’s operators are not magic and they are not built into the type system. Each one is sugar for a trait method:

you write compiler calls
a + b Add::add(a, b)
a - b Sub::sub(a, b)
-a Neg::neg(a)
a[i] *Index::index(&a, i)

That is the whole mechanism. i32 + i32 works because std implements Add for i32, not because the compiler knows about integers.

The surprise

struct Meters(f64);
Meters(3.0) + Meters(1.0)
error[E0369]: cannot add `Meters` to `Meters`
   |
   = note: an implementation of `Add` might be missing for `Meters`

Operators are not inherited from the wrapped type. A newtype around f64 gets nothing from that f64 — not +, not -, not comparison. Everyone hits this exactly once, and it is one of the clearest demonstrations that Rust’s traits are opt-in all the way down. It is also the point: your Meters type refuses to be added to a Seconds by default, and that is the behaviour you wanted when you made the newtype.

Your task

#[derive(Clone, Copy)]
pub struct Vec2 { pub x: f64, pub y: f64 }

impl Add for Vec2 { ... }
impl Sub for Vec2 { ... }
impl Neg for Vec2 { ... }
impl Index<usize> for Vec2 { ... }

pub fn apply(ops: Vec<(String, f64, f64)>) -> Vec<(f64, f64)>

apply runs an accumulator starting at (0.0, 0.0) and emits one pair per operation:

op effect emits
add state = state + (x, y) the new state
sub state = state - (x, y) the new state
neg state = -state the new state
index (state[0], state[1])
other (0.0, 0.0)

Sub, Neg and Index are already written. Add is missing, and the starter therefore fails with the E0369 above. Adding it is the exercise; the rest of the file is there for you to read.

Three details in those impl blocks

type Output is an associated type. Add declares type Output; and each impl decides what it is. For Vec2 + Vec2 the output is a Vec2, but nothing requires that: std implements Mul<f64> for Duration with Output = Duration, and Sub for Instant with Output = Duration. The trait has a shape; the impl fills in the types.

Add has a default type parameter. The real declaration is trait Add<Rhs = Self>. Writing impl Add for Vec2 means impl Add<Vec2> for Vec2. To support Vec2 * f64 you would write impl Mul<f64> for Vec2 — the parameter is what lets one type be addable to several others.

Index returns a reference. The signature is fn index(&self, i: usize) -> &Self::Output, not -> Self::Output. Return the value directly and you get E0308. The reference is what makes v[0] = 1.0 possible for types that also implement IndexMut, and it is why indexing can hand out a borrow of something inside a collection without copying it.

Two clippy gates worth knowing before you trip them

clippy::suspicious_arithmetic_impl is default-on and catches the copy-paste bug you are about to make:

impl Add for Vec2 {
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2 { x: self.x - rhs.x, ... }   // rejected: `-` inside an `Add` impl
    }
}

Almost every real instance of this is a duplicated Sub block someone forgot to finish editing. Its sibling suspicious_op_assign_impl covers +=.

clippy::should_implement_trait is the other half. Define an inherent fn add(self, other: Vec2) on a public type instead of implementing the trait and clippy stops you: “defining a method called add on this type; consider implementing the std::ops::Add trait”. If you named it add, callers will reach for +; give them +.

One design note for later

Add::add takes self by value. That is fine here because Vec2 is Copy — 16 bytes, no allocation. For a type that owns a heap buffer, a + b would consume both operands, which is usually not what a caller wants. The standard answers are to implement Add for &T as well (impl Add<&Matrix> for &Matrix) or to make the value-taking impl clone internally. std does the former for String, which is why s1 + &s2 consumes s1 but only borrows s2.

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