Guide contents

Lesson 34 of 94

Advanced Traits

This lesson covers the trait features you meet in real libraries: associated types (the Item in Iterator<Item = T>), operator overloading, supertraits, calling a specific trait's method when names clash, and the newtype pattern for working around the orphan rule.

1. Associated types

An associated type is a type chosen by each implementation, declared with type Name; in the trait. The standard Iterator trait uses one: type Item; says what the iterator yields.

Rust
trait Container {
    type Item;
    fn get(&self, index: usize) -> Option<&Self::Item>;
    fn first(&self) -> Option<&Self::Item> {
        self.get(0)
    }
}

struct Playlist {
    songs: Vec<String>,
}

struct Scores {
    values: Vec<u32>,
}

impl Container for Playlist {
    type Item = String;
    fn get(&self, index: usize) -> Option<&String> {
        self.songs.get(index)
    }
}

impl Container for Scores {
    type Item = u32;
    fn get(&self, index: usize) -> Option<&u32> {
        self.values.get(index)
    }
}

fn main() {
    let p = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] };
    let s = Scores { values: vec![98, 87] };
    println!("{:?} {:?}", p.first(), p.get(1));
    println!("{:?} {:?}", s.first(), s.get(5));
}
Output
Some("Intro") Some("Outro")
Some(98) None

2. Implementing Iterator with its associated type

Iterator has one required method, next, which returns Option<Self::Item>. Custom Iterators goes much deeper; this is the minimum.

Rust
struct Countdown {
    remaining: u32,
}

impl Iterator for Countdown {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.remaining == 0 {
            return None;
        }
        self.remaining -= 1;
        Some(self.remaining + 1)
    }
}

fn main() {
    for n in (Countdown { remaining: 3 }) {
        print!("{n}... ");
    }
    println!("liftoff");
    let total: u32 = Countdown { remaining: 10 }.sum(); // one of Iterator's ~70 default methods
    println!("sum = {total}");
}
Output
3... 2... 1... liftoff
sum = 55

3. Associated types vs generic type parameters

A generic trait (trait Convert<T>) can be implemented many times for one type, once per T. A trait with an associated type can be implemented only once per type, so the type is fixed. Use an associated type when each implementing type has exactly one natural choice, as with Iterator::Item.

Rust
// Generic trait: one type can implement it for many targets.
trait ConvertTo<T> {
    fn convert(&self) -> T;
}

struct Celsius(f64);

impl ConvertTo<f64> for Celsius {
    fn convert(&self) -> f64 {
        self.0
    }
}

impl ConvertTo<String> for Celsius {
    fn convert(&self) -> String {
        format!("{:.1}°C", self.0)
    }
}

// Associated type: exactly one output per implementing type.
trait Parse {
    type Output;
    fn parse(input: &str) -> Option<Self::Output>;
}

struct Port;

impl Parse for Port {
    type Output = u16;
    fn parse(input: &str) -> Option<u16> {
        input.parse().ok().filter(|&p| p >= 1)
    }
}

fn main() {
    let t = Celsius(21.456);
    let as_number: f64 = t.convert();
    let as_text: String = t.convert();
    println!("{as_number} / {as_text}");
    println!("{:?} {:?}", Port::parse("8080"), Port::parse("0"));
}
Output
21.456 / 21.5°C
Some(8080) None

With the generic trait, the caller had to say which conversion it wanted (through the variable's type). With the associated type, there's nothing to choose.

4. Constraining an associated type in a bound

I: Iterator<Item = u32> means "any iterator whose Item is u32".

Rust
fn total_bytes<I: Iterator<Item = u64>>(sizes: I) -> u64 {
    let mut total = 0;
    for s in sizes {
        total += s;
    }
    total
}

fn main() {
    let files = vec![1_024_u64, 2_048, 512];
    println!("{}", total_bytes(files.into_iter()));
    println!("{}", total_bytes((1..=4).map(|kb| kb * 1_000)));
}
Output
3584
10000

5. -> impl Trait in trait methods

A trait method can return impl Trait (since Rust 1.75). Each implementation then returns its own concrete type, and callers only see the trait. This is the usual way to return an iterator from a trait method without boxing it:

Rust
use std::collections::HashMap;

trait Catalog {
    fn product_names(&self) -> impl Iterator<Item = &str>;

    // Default methods can use it like any iterator.
    fn count_starting_with(&self, prefix: &str) -> usize {
        self.product_names().filter(|name| name.starts_with(prefix)).count()
    }
}

struct ListCatalog {
    products: Vec<(String, bool)>, // (name, in stock)
}

struct MapCatalog {
    prices: HashMap<String, u32>,
}

impl Catalog for ListCatalog {
    // A Filter<Map<...>> type, never written out.
    fn product_names(&self) -> impl Iterator<Item = &str> {
        self.products.iter().filter(|(_, in_stock)| *in_stock).map(|(name, _)| name.as_str())
    }
}

impl Catalog for MapCatalog {
    // A different type: a Map over HashMap keys.
    fn product_names(&self) -> impl Iterator<Item = &str> {
        self.prices.keys().map(String::as_str)
    }
}

fn main() {
    let list = ListCatalog {
        products: vec![("lamp".into(), true), ("ladder".into(), false), ("mug".into(), true)],
    };
    let map = MapCatalog { prices: HashMap::from([("laptop".to_string(), 999), ("lens".to_string(), 250)]) };

    println!("{:?}", list.product_names().collect::<Vec<_>>());
    let mut names: Vec<&str> = map.product_names().collect();
    names.sort();
    println!("{names:?}");
    println!("{} {}", list.count_starting_with("l"), map.count_starting_with("l"));
}
Output
["lamp", "mug"]
["laptop", "lens"]
1 2

Like generic methods, a method returning impl Trait makes the trait unusable as dyn Catalog. When you need dyn, return Box<dyn Iterator<Item = &str> + '_> instead. async fn in traits, covered in Async Traits, is built on exactly this feature: it returns impl Future.

6. Supertraits

trait Entity: Debug + Clone means "to implement Entity, a type must also implement Debug and Clone". Code using Entity can then rely on those traits too.

Rust
use std::fmt::Debug;

trait Entity: Debug + Clone {
    fn id(&self) -> u64;

    fn snapshot(&self) -> (u64, Self) {
        (self.id(), self.clone())
    }
}

#[derive(Debug, Clone)]
struct Customer {
    id: u64,
    name: String,
}

impl Entity for Customer {
    fn id(&self) -> u64 {
        self.id
    }
}

fn audit<E: Entity>(e: &E) {
    let (id, copy) = e.snapshot();
    println!("audited #{id}: {:?}", copy); // Debug is guaranteed by the supertrait
}

fn main() {
    audit(&Customer { id: 7, name: String::from("Acme") });
}
Output
audited #7: Customer { id: 7, name: "Acme" }

7. Implementing a trait without its supertrait fails

Rust✗ does not compile · E0277
trait Named {
    fn name(&self) -> String;
}

trait Greeter: Named {
    fn greet(&self) -> String {
        format!("Hello, {}", self.name())
    }
}

struct Robot;

impl Greeter for Robot {}

fn main() {}
Output
error[E0277]: the trait bound `Robot: Named` is not satisfied
  --> src/main.rs:13:18
   |
13 | impl Greeter for Robot {}
   |                  ^^^^^ unsatisfied trait bound
   |
help: the trait `Named` is not implemented for `Robot`
  --> src/main.rs:11:1
   |
11 | struct Robot;
   | ^^^^^^^^^^^^
help: this trait has no implementations, consider adding one
  --> src/main.rs:1:1
   |
 1 | trait Named {
   | ^^^^^^^^^^^
note: required by a bound in `Greeter`
  --> src/main.rs:5:16
   |
 5 | trait Greeter: Named {
   |                ^^^^^ required by this bound in `Greeter`

This is not inheritance. A supertrait is a requirement: Robot must implement Named separately, with its own impl block.

8. Operator overloading: Add

Every operator is a trait in std::ops. Implementing Add makes a + b work for your type. The associated type Output says what the result is.

Rust
use std::ops::Add;

#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
    cents: i64,
}

impl Add for Money {
    type Output = Money;

    fn add(self, other: Money) -> Money {
        Money { cents: self.cents + other.cents }
    }
}

fn main() {
    let price = Money { cents: 1_999 };
    let shipping = Money { cents: 499 };
    let total = price + shipping;
    println!("{:?}", total);
    println!("{}", total == Money { cents: 2_498 });
}
Output
Money { cents: 2498 }
true

Rust lets domain types read like arithmetic.

9. Adding different types: Add<Rhs>

Add has a type parameter for the right-hand side, which defaults to Self. Implement Add<i64> to allow Money + i64.

Rust
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Money {
    cents: i64,
}

impl Add<i64> for Money {
    type Output = Money;
    fn add(self, cents: i64) -> Money {
        Money { cents: self.cents + cents }
    }
}

fn main() {
    let tip = Money { cents: 1_000 } + 250;
    println!("{:?}", tip);
}
Output
Money { cents: 1250 }

The trait is declared as trait Add<Rhs = Self>. Rhs = Self is a default type parameter, which is why plain impl Add for Money means impl Add<Money> for Money.

10. Sub, Mul, and Neg

Rust
use std::ops::{Mul, Neg, Sub};

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

impl Sub for Vec2 {
    type Output = Vec2;
    fn sub(self, o: Vec2) -> Vec2 {
        Vec2 { x: self.x - o.x, y: self.y - o.y }
    }
}

impl Mul<f64> for Vec2 {
    type Output = Vec2;
    fn mul(self, k: f64) -> Vec2 {
        Vec2 { x: self.x * k, y: self.y * k }
    }
}

impl Neg for Vec2 {
    type Output = Vec2;
    fn neg(self) -> Vec2 {
        Vec2 { x: -self.x, y: -self.y }
    }
}

fn main() {
    let target = Vec2 { x: 10.0, y: 4.0 };
    let position = Vec2 { x: 2.0, y: 1.0 };
    let direction = target - position;
    println!("{:?}", direction);
    println!("{:?}", direction * 0.5);
    println!("{:?}", -direction);
}
Output
Vec2 { x: 8.0, y: 3.0 }
Vec2 { x: 4.0, y: 1.5 }
Vec2 { x: -8.0, y: -3.0 }

11. Compound assignment: AddAssign

+= is a separate trait, AddAssign, which changes the left side in place.

Rust
use std::ops::AddAssign;

#[derive(Debug, Default)]
struct Totals {
    orders: u32,
    revenue_cents: u64,
}

impl AddAssign<u64> for Totals {
    fn add_assign(&mut self, order_cents: u64) {
        self.orders += 1;
        self.revenue_cents += order_cents;
    }
}

fn main() {
    let mut t = Totals::default();
    for cents in [1_999, 4_500, 250] {
        t += cents;
    }
    println!("{:?}", t);
}
Output
Totals { orders: 3, revenue_cents: 6749 }

12. Indexing with Index and IndexMut

container[key] calls Index::index, and assignment through it calls IndexMut::index_mut. Here a grid is indexed by (row, column) tuples.

Rust
use std::ops::{Index, IndexMut};

struct Grid {
    width: usize,
    cells: Vec<char>,
}

impl Grid {
    fn new(width: usize, height: usize) -> Self {
        Self { width, cells: vec!['.'; width * height] }
    }
}

impl Index<(usize, usize)> for Grid {
    type Output = char;
    fn index(&self, (row, col): (usize, usize)) -> &char {
        &self.cells[row * self.width + col]
    }
}

impl IndexMut<(usize, usize)> for Grid {
    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut char {
        &mut self.cells[row * self.width + col]
    }
}

fn main() {
    let mut g = Grid::new(4, 3);
    g[(0, 0)] = 'S';
    g[(2, 3)] = 'E';
    g[(1, 1)] = '#';
    for row in 0..3 {
        let line: String = (0..4).map(|col| g[(row, col)]).collect();
        println!("{line}");
    }
    println!("start is {}", g[(0, 0)]);
}
Output
S...
.#..
...E
start is S

13. The Sum trait: making .sum() work for your type

Iterator's .sum() works for any type implementing std::iter::Sum.

Rust
use std::iter::Sum;
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Duration {
    secs: u64,
}

impl Add for Duration {
    type Output = Duration;
    fn add(self, o: Duration) -> Duration {
        Duration { secs: self.secs + o.secs }
    }
}

impl Sum for Duration {
    fn sum<I: Iterator<Item = Duration>>(iter: I) -> Duration {
        iter.fold(Duration { secs: 0 }, |a, b| a + b)
    }
}

fn main() {
    let laps = vec![Duration { secs: 62 }, Duration { secs: 58 }, Duration { secs: 61 }];
    let total: Duration = laps.iter().copied().sum();
    println!("{:?} = {}m{}s", total, total.secs / 60, total.secs % 60);
}
Output
Duration { secs: 181 } = 3m1s

fold combines all items into one value. Iterator Adapters covers it.

14. Operators on references

Implementing Add for Money consumes both operands. For non-Copy types, also implement it for references so &a + &b works without moving.

Rust
use std::ops::Add;

#[derive(Debug, Clone, PartialEq)]
struct Tags(Vec<String>);

impl Add for &Tags {
    type Output = Tags;
    fn add(self, other: &Tags) -> Tags {
        let mut all = self.0.clone();
        for t in &other.0 {
            if !all.contains(t) {
                all.push(t.clone());
            }
        }
        Tags(all)
    }
}

fn main() {
    let a = Tags(vec![String::from("rust"), String::from("cli")]);
    let b = Tags(vec![String::from("cli"), String::from("async")]);
    let merged = &a + &b;
    println!("{:?}", merged);
    println!("originals intact: {:?} {:?}", a, b);
}
Output
Tags(["rust", "cli", "async"])
originals intact: Tags(["rust", "cli"]) Tags(["cli", "async"])

15. Two traits with the same method name

A type can implement two traits that both define a method called name. Calling x.name() is then ambiguous, and Rust asks you to say which one you mean.

Rust✗ does not compile · E0034
trait Pilot {
    fn name(&self) -> String;
}

trait Wizard {
    fn name(&self) -> String;
}

struct Human;

impl Pilot for Human {
    fn name(&self) -> String { String::from("Captain") }
}

impl Wizard for Human {
    fn name(&self) -> String { String::from("Merlin") }
}

fn main() {
    println!("{}", Human.name());
}
Output
error[E0034]: multiple applicable items in scope
  --> src/main.rs:20:26
   |
20 |     println!("{}", Human.name());
   |                          ^^^^ multiple `name` found
   |
note: candidate #1 is defined in an impl of the trait `Pilot` for the type `Human`
  --> src/main.rs:12:5
   |
12 |     fn name(&self) -> String { String::from("Captain") }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^
note: candidate #2 is defined in an impl of the trait `Wizard` for the type `Human`
  --> src/main.rs:16:5
   |
16 |     fn name(&self) -> String { String::from("Merlin") }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^
help: disambiguate the method for candidate #1
   |
20 -     println!("{}", Human.name());
20 +     println!("{}", Pilot::name(&Human));
   |
help: disambiguate the method for candidate #2
   |
20 -     println!("{}", Human.name());
20 +     println!("{}", Wizard::name(&Human));
   |

16. Disambiguating with Trait::method(&x)

Rust
trait Pilot {
    fn name(&self) -> String;
}

trait Wizard {
    fn name(&self) -> String;
}

struct Human;

impl Pilot for Human {
    fn name(&self) -> String {
        String::from("Captain")
    }
}

impl Wizard for Human {
    fn name(&self) -> String {
        String::from("Merlin")
    }
}

impl Human {
    fn name(&self) -> String {
        String::from("Arthur")
    }
}

fn main() {
    let h = Human;
    println!("{}", h.name());           // the inherent method wins
    println!("{}", Pilot::name(&h));
    println!("{}", Wizard::name(&h));
}
Output
Arthur
Captain
Merlin

17. Fully qualified syntax for associated functions

Associated functions have no self to tell the traits apart, so you write the full form: <Type as Trait>::function().

Rust
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("{}", Dog::baby_name());
    println!("{}", <Dog as Animal>::baby_name());
}
Output
Spot
puppy

18. The newtype pattern: implementing foreign traits for foreign types

The orphan rule forbids impl Display for Vec<String>. Wrapping the Vec in your own tuple struct makes the type local, so any trait can be implemented for it.

Rust
use std::fmt;

struct Csv(Vec<String>);

impl fmt::Display for Csv {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let escaped: Vec<String> = self
            .0
            .iter()
            .map(|field| if field.contains(',') { format!("\"{field}\"") } else { field.clone() })
            .collect();
        write!(f, "{}", escaped.join(","))
    }
}

fn main() {
    let row = Csv(vec![String::from("42"), String::from("Hopper, Grace"), String::from("admiral")]);
    println!("{row}");
    println!("{} fields", row.0.len());
}
Output
42,"Hopper, Grace",admiral
3 fields

The wrapper costs nothing at runtime. The downside is that Csv doesn't have Vec's methods; you reach them through .0, or by implementing Deref (Drop, Deref & Cow).

19. Newtypes to add meaning and restrict operations

A newtype can deliberately expose only a few operations. Here Meters can be added to Meters and scaled, but not added to Seconds.

Rust
use std::ops::{Add, Mul};

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);

#[derive(Debug, Clone, Copy)]
struct Seconds(f64);

impl Add for Meters {
    type Output = Meters;
    fn add(self, o: Meters) -> Meters {
        Meters(self.0 + o.0)
    }
}

impl Mul<f64> for Meters {
    type Output = Meters;
    fn mul(self, k: f64) -> Meters {
        Meters(self.0 * k)
    }
}

fn speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}

fn main() {
    let leg1 = Meters(1_200.0);
    let leg2 = Meters(800.0);
    let total = leg1 + leg2;
    println!("{:?}, doubled {:?}", total, total * 2.0);
    println!("{:.2} m/s", speed(total, Seconds(600.0)));
    // let nonsense = leg1 + Seconds(5.0); // does not compile
}
Output
Meters(2000.0), doubled Meters(4000.0)
3.33 m/s

20. Adding meters to seconds is rejected

Rust✗ does not compile · E0308
use std::ops::Add;

#[derive(Clone, Copy)]
struct Meters(f64);

#[derive(Clone, Copy)]
struct Seconds(f64);

impl Add for Meters {
    type Output = Meters;
    fn add(self, o: Meters) -> Meters { Meters(self.0 + o.0) }
}

fn main() {
    let x = Meters(5.0) + Seconds(2.0);
}
Output
error[E0308]: mismatched types
  --> src/main.rs:15:27
   |
15 |     let x = Meters(5.0) + Seconds(2.0);
   |             -----------   ^^^^^^^^^^^^ expected `Meters`, found `Seconds`
   |             |
   |             expected because this is `Meters`

21. Marker traits

A marker trait has no methods. It labels types as having some property, and bounds can require it. Copy, Send, and Sync are standard marker traits.

Rust
trait Validated {}

#[derive(Debug)]
struct Email(String);

impl Validated for Email {}

#[derive(Debug)]
struct RawInput(String);

fn store<T: Validated + std::fmt::Debug>(value: T) {
    println!("stored {:?}", value);
}

fn validate(input: RawInput) -> Option<Email> {
    if input.0.contains('@') { Some(Email(input.0)) } else { None }
}

fn main() {
    if let Some(email) = validate(RawInput(String::from("ana@example.com"))) {
        store(email);
    }
    // store(RawInput(String::from("x"))); // does not compile: RawInput is not Validated
}
Output
stored Email("ana@example.com")

22. Sealed traits

Sometimes a library wants a trait others can use but not implement, so new methods can be added later without breaking anyone. The trick: a public trait with a supertrait defined in a private module. Outside code can't name the private trait, so it can't implement the public one.

Rust
mod shapes {
    mod private {
        pub trait Sealed {}
    }

    pub trait Shape: private::Sealed {
        fn sides(&self) -> u32;
    }

    pub struct Triangle;
    pub struct Square;

    impl private::Sealed for Triangle {}
    impl private::Sealed for Square {}

    impl Shape for Triangle {
        fn sides(&self) -> u32 {
            3
        }
    }

    impl Shape for Square {
        fn sides(&self) -> u32 {
            4
        }
    }
}

use shapes::Shape;

fn main() {
    println!("{} {}", shapes::Triangle.sides(), shapes::Square.sides());
}
Output
3 4

Outside the shapes module, impl Shape for MyType fails because private::Sealed can't be implemented or even named.

23. Forwarding a trait through &T and Box<T>

If a function takes impl Describe, callers might have a &T or a Box<T>. Implementing the trait for those pointer types once makes them all work. The standard library does this for most of its traits.

Rust
trait Describe {
    fn describe(&self) -> String;
}

struct Server(&'static str);

impl Describe for Server {
    fn describe(&self) -> String {
        format!("server {}", self.0)
    }
}

impl<T: Describe + ?Sized> Describe for &T {
    fn describe(&self) -> String {
        (**self).describe()
    }
}

impl<T: Describe + ?Sized> Describe for Box<T> {
    fn describe(&self) -> String {
        (**self).describe()
    }
}

fn log(item: impl Describe) {
    println!("{}", item.describe());
}

fn main() {
    let s = Server("db-1");
    log(&s);
    log(Box::new(Server("db-2")));
    log(s);
}
Output
server db-1
server db-2
server db-1

24. Realistic example: a complete Money type

A value type with arithmetic, comparison, display, and summing. Adding money in two different currencies is caught with a Result-returning method, while same-currency arithmetic reads naturally.

Rust
use std::fmt;
use std::iter::Sum;
use std::ops::{Add, Mul, Sub};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
struct Usd {
    cents: i64,
}

impl Usd {
    fn new(dollars: i64, cents: i64) -> Self {
        Self { cents: dollars * 100 + cents }
    }
}

impl Add for Usd {
    type Output = Usd;
    fn add(self, o: Usd) -> Usd {
        Usd { cents: self.cents + o.cents }
    }
}

impl Sub for Usd {
    type Output = Usd;
    fn sub(self, o: Usd) -> Usd {
        Usd { cents: self.cents - o.cents }
    }
}

impl Mul<i64> for Usd {
    type Output = Usd;
    fn mul(self, qty: i64) -> Usd {
        Usd { cents: self.cents * qty }
    }
}

impl Sum for Usd {
    fn sum<I: Iterator<Item = Usd>>(iter: I) -> Usd {
        iter.fold(Usd::default(), |a, b| a + b)
    }
}

impl fmt::Display for Usd {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let sign = if self.cents < 0 { "-" } else { "" };
        f.pad(&format!("{sign}${}.{:02}", self.cents.abs() / 100, self.cents.abs() % 100))
    }
}

fn main() {
    let lines = [(Usd::new(12, 50), 2), (Usd::new(3, 99), 3), (Usd::new(45, 0), 1)];
    for (price, qty) in lines {
        println!("{qty} x {price:>7} = {:>8}", price * qty);
    }
    let subtotal: Usd = lines.iter().map(|&(p, q)| p * q).sum();
    let discount = Usd::new(10, 0);
    let total = subtotal - discount;
    println!("subtotal {subtotal:>9}");
    println!("discount {:>9}", Usd { cents: -discount.cents });
    println!("total    {total:>9}");
    println!("over $70? {}", total > Usd::new(70, 0));
}
Output
2 x  $12.50 =   $25.00
3 x   $3.99 =   $11.97
1 x  $45.00 =   $45.00
subtotal    $81.97
discount   -$10.00
total       $71.97
over $70? true

25. Realistic example: 2D vector math for a game

Rust
use std::ops::{Add, AddAssign, Mul, Sub};

#[derive(Debug, Clone, Copy, PartialEq, Default)]
struct V2 {
    x: f64,
    y: f64,
}

impl Add for V2 {
    type Output = V2;
    fn add(self, o: V2) -> V2 {
        V2 { x: self.x + o.x, y: self.y + o.y }
    }
}

impl Sub for V2 {
    type Output = V2;
    fn sub(self, o: V2) -> V2 {
        V2 { x: self.x - o.x, y: self.y - o.y }
    }
}

impl Mul<f64> for V2 {
    type Output = V2;
    fn mul(self, k: f64) -> V2 {
        V2 { x: self.x * k, y: self.y * k }
    }
}

impl AddAssign for V2 {
    fn add_assign(&mut self, o: V2) {
        self.x += o.x;
        self.y += o.y;
    }
}

impl V2 {
    fn length(self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

fn main() {
    let gravity = V2 { x: 0.0, y: -9.8 };
    let mut position = V2 { x: 0.0, y: 20.0 };
    let mut velocity = V2 { x: 3.0, y: 0.0 };
    let dt = 0.5;
    for step in 1..=4 {
        velocity += gravity * dt;
        position += velocity * dt;
        println!("t={:.1}s pos=({:.2}, {:.2}) speed={:.2}", step as f64 * dt, position.x, position.y, velocity.length());
    }
    let to_origin = V2::default() - position;
    println!("distance to origin: {:.2}", to_origin.length());
}
Output
t=0.5s pos=(1.50, 17.55) speed=5.75
t=1.0s pos=(3.00, 12.65) speed=10.25
t=1.5s pos=(4.50, 5.30) speed=15.00
t=2.0s pos=(6.00, -4.50) speed=19.83
distance to origin: 7.50

26. Realistic example: a sparse matrix with Index

A sparse matrix stores only non-zero entries. Index returns a reference, so for missing entries it returns a reference to a shared static zero.

Rust
use std::ops::Index;

struct Sparse {
    rows: usize,
    cols: usize,
    entries: Vec<((usize, usize), f64)>,
}

static ZERO: f64 = 0.0;

impl Sparse {
    fn set(&mut self, r: usize, c: usize, v: f64) {
        assert!(r < self.rows && c < self.cols, "({r}, {c}) out of bounds");
        self.entries.retain(|(pos, _)| *pos != (r, c));
        if v != 0.0 {
            self.entries.push(((r, c), v));
        }
    }
}

impl Index<(usize, usize)> for Sparse {
    type Output = f64;
    fn index(&self, pos: (usize, usize)) -> &f64 {
        self.entries.iter().find(|(p, _)| *p == pos).map(|(_, v)| v).unwrap_or(&ZERO)
    }
}

fn main() {
    let mut m = Sparse { rows: 1_000, cols: 1_000, entries: Vec::new() };
    m.set(0, 0, 1.5);
    m.set(999, 42, -3.0);
    m.set(0, 0, 0.0); // setting zero removes the entry
    println!("{} {} {}", m[(0, 0)], m[(999, 42)], m[(500, 500)]);
    println!("stored entries: {} of {}", m.entries.len(), m.rows * m.cols);
}
Output
0 -3 0
stored entries: 1 of 1000000

assert! panics with the message if the condition is false: a check for bugs that is always enabled, even in release builds.

27. Realistic example: a key-value store trait with associated types

Each backend chooses its own key and value types, and generic code works with any backend.

Rust
use std::fmt::Debug;

trait Store {
    type Key: Debug + PartialEq + Clone;
    type Value: Debug + Clone;

    fn put(&mut self, key: Self::Key, value: Self::Value);
    fn get(&self, key: &Self::Key) -> Option<Self::Value>;
}

struct SessionStore {
    sessions: Vec<(u64, String)>,
}

struct FeatureFlags {
    flags: Vec<(&'static str, bool)>,
}

impl Store for SessionStore {
    type Key = u64;
    type Value = String;
    fn put(&mut self, key: u64, value: String) {
        self.sessions.retain(|(k, _)| *k != key);
        self.sessions.push((key, value));
    }
    fn get(&self, key: &u64) -> Option<String> {
        self.sessions.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
    }
}

impl Store for FeatureFlags {
    type Key = &'static str;
    type Value = bool;
    fn put(&mut self, key: &'static str, value: bool) {
        self.flags.retain(|(k, _)| *k != key);
        self.flags.push((key, value));
    }
    fn get(&self, key: &&'static str) -> Option<bool> {
        self.flags.iter().find(|(k, _)| k == key).map(|(_, v)| *v)
    }
}

fn copy_entry<S: Store>(store: &mut S, from: S::Key, to: S::Key) -> bool {
    match store.get(&from) {
        Some(v) => {
            println!("copying {:?} -> {:?}: {:?}", from, to, v);
            store.put(to, v);
            true
        }
        None => false,
    }
}

fn main() {
    let mut sessions = SessionStore { sessions: vec![(1, String::from("user=ana"))] };
    copy_entry(&mut sessions, 1, 2);
    println!("{:?}", sessions.get(&2));

    let mut flags = FeatureFlags { flags: vec![("new_checkout", true)] };
    copy_entry(&mut flags, "new_checkout", "new_checkout_eu");
    println!("{:?} {:?}", flags.get(&"new_checkout_eu"), flags.get(&"dark_mode"));
}
Output
copying 1 -> 2: "user=ana"
Some("user=ana")
copying "new_checkout" -> "new_checkout_eu": true
Some(true) None

Bounds on associated types (type Key: Debug + PartialEq + Clone;) let generic code like copy_entry print, compare, and clone keys without knowing their concrete types.

28. Realistic example: temperature units with conversions and comparison

Rust
use std::cmp::Ordering;
use std::fmt;

#[derive(Debug, Clone, Copy)]
enum Temp {
    C(f64),
    F(f64),
    K(f64),
}

impl Temp {
    fn kelvin(self) -> f64 {
        match self {
            Temp::C(c) => c + 273.15,
            Temp::F(f) => (f - 32.0) * 5.0 / 9.0 + 273.15,
            Temp::K(k) => k,
        }
    }
}

impl PartialEq for Temp {
    fn eq(&self, other: &Self) -> bool {
        (self.kelvin() - other.kelvin()).abs() < 1e-9
    }
}

impl PartialOrd for Temp {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.kelvin().partial_cmp(&other.kelvin())
    }
}

impl fmt::Display for Temp {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Temp::C(v) => write!(f, "{v:.1}°C"),
            Temp::F(v) => write!(f, "{v:.1}°F"),
            Temp::K(v) => write!(f, "{v:.1}K"),
        }
    }
}

fn main() {
    let readings = [Temp::C(21.0), Temp::F(75.0), Temp::K(290.0), Temp::F(69.8)];
    let hottest = readings.iter().fold(readings[0], |a, &b| if b > a { b } else { a });
    println!("hottest: {hottest}");
    println!("{} == {}: {}", Temp::C(21.0), Temp::F(69.8), Temp::C(21.0) == Temp::F(69.8));
    println!("{} < {}: {}", Temp::K(290.0), Temp::C(21.0), Temp::K(290.0) < Temp::C(21.0));
}
Output
hottest: 75.0°F
21.0°C == 69.8°F: true
290.0K < 21.0°C: true

Summary

  • Associated types (type Item;) let each implementation choose one type. Use them when there is one natural choice per type; use a generic trait (Trait<T>) when a type should implement it several times.
  • Supertraits (trait A: B + C) require implementors to also implement other traits.
  • Operators are traits in std::ops: Add, Sub, Mul, Neg, AddAssign, Index, IndexMut, and more. Sum makes .sum() work.
  • When method names clash, call Trait::method(&x), or <Type as Trait>::function() for associated functions.
  • The newtype pattern wraps a foreign type so you can implement foreign traits for it, and adds meaning and restrictions at zero cost.
  • Marker traits label types; sealed traits can be used but not implemented outside their module.

Next: Conversion Traits.