Generics, Traits and Lifetimes · about 40 min

The Standard Traits and derive

Why this matters

The standard library defines a set of traits that the whole ecosystem relies on: printing, copying, comparing, hashing, default values, conversions, operators. When your types implement them, they work with {:?}, ==, sort, HashMap keys, ? conversions and +, just like built-in types. Most can be generated automatically with #[derive(...)], which you have been writing as a recipe since module 7. This lesson explains what each one means and when it can be derived.

derive

#[derive(Trait1, Trait2)] asks the compiler to write the impl for you, field by field:

#[derive(Debug, Clone, PartialEq, Default)]
struct Config {
    name: String,
    retries: u32,
    verbose: bool,
}

fn main() {
    let a = Config { name: String::from("prod"), retries: 3, verbose: false };
    let b = a.clone();
    let c = Config::default();
    println!("{:?}", a);
    println!("{} {}", a == b, a == c);
    println!("{:?}", c);
    let d = Config { retries: 5, ..Default::default() };
    println!("{:?}", d);
}
Config { name: "prod", retries: 3, verbose: false }
true false
Config { name: "", retries: 0, verbose: false }
Config { name: "", retries: 5, verbose: false }

A derive only works if every field implements the trait; the generated code calls it on each field.

The traits, one by one

Debug: {:?} formatting, for programmers. Derive it on nearly every type; it costs nothing and makes debugging easier.

Clone: explicit duplication with .clone(). Derived as "clone each field".

Copy: implicit bitwise copy on assignment (module 4). Requires Clone, and every field must be Copy. Only for small, plain data with no heap ownership: points, colors, IDs.

PartialEq / Eq: == and !=. Derived as "all fields equal". Eq adds the promise that every value equals itself, which floats break (NaN != NaN), so types with f64 fields can be PartialEq but not Eq. HashMap keys need Eq.

PartialOrd / Ord: <, >, sort, max, BTreeMap keys. Derived as lexicographic order: compare the first field; if equal, the second; and so on (for enums, earlier variants are smaller). Field order in the struct definition therefore matters. Ord is a total order and requires Eq; floats again only manage Partial.

Hash: lets a value be hashed for HashMap/HashSet. Must agree with Eq (equal values must have equal hashes); deriving both keeps them consistent.

Default: Type::default(), a sensible "empty" value (zeros, empty strings, None, false).

use std::collections::HashSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Suit { Clubs, Diamonds, Hearts, Spades }

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Card { rank: u8, suit: Suit }

fn main() {
    let mut hand = vec![
        Card { rank: 12, suit: Suit::Hearts },
        Card { rank: 3, suit: Suit::Spades },
        Card { rank: 12, suit: Suit::Clubs },
    ];
    hand.sort();
    println!("{:?}", hand);
    println!("{}", Suit::Clubs < Suit::Spades);
    let unique: HashSet<Card> = hand.iter().copied().collect();
    println!("{} unique", unique.len());
}
[Card { rank: 3, suit: Spades }, Card { rank: 12, suit: Clubs }, Card { rank: 12, suit: Hearts }]
true
3 unique

Cards sort by rank first (the first field), then by suit in variant order.

Implementing by hand

Derive when the field-by-field behavior is right; implement manually when it is not. A common case: equality that ignores some fields, or a custom order. Here Display (never derivable, since there is no single right user-facing format) and an Ord that sorts by score descending:

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

#[derive(Debug, PartialEq, Eq)]
struct Player { name: String, score: u32 }

impl Ord for Player {
    fn cmp(&self, other: &Self) -> Ordering {
        other.score.cmp(&self.score).then_with(|| self.name.cmp(&other.name))
    }
}

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

impl fmt::Display for Player {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} ({})", self.name, self.score)
    }
}

fn main() {
    let mut ps = vec![
        Player { name: String::from("bo"), score: 7 },
        Player { name: String::from("al"), score: 9 },
        Player { name: String::from("cy"), score: 7 },
    ];
    ps.sort();
    for p in &ps {
        println!("{p}");
    }
}
al (9)
bo (7)
cy (7)

cmp returns an Ordering (Less, Equal, Greater); then_with breaks ties. Swapping other and self reverses the order.

Conversions: From and Into

From<T> for U defines how to build a U from a T. Implementing it gives you Into for free (another blanket impl), and ? uses From to convert errors (module 8):

#[derive(Debug)]
struct Celsius(f64);

impl From<f64> for Celsius {
    fn from(v: f64) -> Self {
        Celsius(v)
    }
}

fn main() {
    let a = Celsius::from(21.5);
    let b: Celsius = 30.0.into();
    let s = String::from("text");
    let n = i64::from(42i32);
    println!("{:?} {:?} {s} {n}", a, b);
}
Celsius(21.5) Celsius(30.0) text 42

From is for conversions that cannot fail. For fallible ones there is TryFrom (u8::try_from(300) returns an Err).

Operators are traits

a + b calls std::ops::Add::add(a, b); a == b calls PartialEq::eq; a[i] calls Index::index. Implementing the trait gives your type the operator:

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

#[derive(Debug, Clone, Copy, PartialEq)]
struct Money(i64);

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

impl Mul<i64> for Money {
    type Output = Money;
    fn mul(self, n: i64) -> Money {
        Money(self.0 * n)
    }
}

fn main() {
    let price = Money(250);
    let total = price * 3 + Money(100);
    println!("{:?}", total);
}
Money(850)

type Output = Money; is an associated type: part of the trait that each implementation fills in (here, the result type of +). Iterator's type Item is another example. Overload operators only where the meaning is obvious (math types, money); a surprising + makes code harder to read.

Common mistakes

Recap

Going further: read the docs for std::cmp::Ordering and try sort_by_key(|p| std::cmp::Reverse(p.score)) as an alternative to a manual Ord.

Exercises

Exercise 1 · write code

Derive to sort and dedupe

struct Version { major: u32, minor: u32, patch: u32 } needs to be sortable, comparable, usable in a BTreeSet, and printable with {:?}. Add the right #[derive(...)] (field order already gives the correct ordering) and implement Display as MAJOR.MINOR.PATCH. The given main reads versions like 1.10.0 from stdin, removes duplicates, and prints them sorted, then the newest:

0.9.12
1.2.0
1.10.0
newest: 1.10.0

for the input lines 1.10.0, 0.9.12, 1.2.0, 1.10.0.

File: main.rs

Starting point

use std::collections::BTreeSet;
use std::fmt;
use std::io;

struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}

fn parse(s: &str) -> Version {
    let p: Vec<u32> = s.trim().split('.').map(|x| x.parse().unwrap()).collect();
    Version { major: p[0], minor: p[1], patch: p[2] }
}

fn main() {
    let mut set = BTreeSet::new();
    for line in io::stdin().lines() {
        set.insert(parse(&line.unwrap()));
    }
    for v in &set {
        println!("{v}");
    }
    println!("newest: {}", set.iter().max().unwrap());
}

Example

input

1.10.0
0.9.12
1.2.0
1.10.0

expected output

0.9.12
1.2.0
1.10.0
newest: 1.10.0
Hint 1

A BTreeSet and max need Ord, which requires Eq, PartialEq and PartialOrd too.

Hint 2

Derived ordering compares fields top to bottom: major, then minor, then patch. Exactly what versions need (numerically, so 10 > 2).

Reference solution
use std::collections::BTreeSet;
use std::fmt;
use std::io;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

fn parse(s: &str) -> Version {
    let p: Vec<u32> = s.trim().split('.').map(|x| x.parse().unwrap()).collect();
    Version { major: p[0], minor: p[1], patch: p[2] }
}

fn main() {
    let mut set = BTreeSet::new();
    for line in io::stdin().lines() {
        set.insert(parse(&line.unwrap()));
    }
    for v in &set {
        println!("{v}");
    }
    println!("newest: {}", set.iter().max().unwrap());
}

Exercise 2 · write code

Operator overloading with Add

Implement std::ops::Add for #[derive(Debug, Clone, Copy, PartialEq)] struct Vec2 { x: f64, y: f64 } so that a + b adds component-wise, and implement From<(f64, f64)> for Vec2. The given main prints:

Vec2 { x: 4.0, y: 1.5 }
true

File: main.rs

Starting point

use std::ops::Add;

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

fn main() {
    let a = Vec2::from((1.0, 2.0));
    let b: Vec2 = (3.0, -0.5).into();
    println!("{:?}", a + b);
    println!("{}", a + b == Vec2 { x: 4.0, y: 1.5 });
}

Example

expected output

Vec2 { x: 4.0, y: 1.5 }
true
Hint 1

impl Add for Vec2 { type Output = Vec2; fn add(self, other: Vec2) -> Vec2 { ... } }

Hint 2

Implementing From<(f64, f64)> gives you .into() for free.

Reference solution
use std::ops::Add;

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

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

impl From<(f64, f64)> for Vec2 {
    fn from(t: (f64, f64)) -> Self {
        Vec2 { x: t.0, y: t.1 }
    }
}

fn main() {
    let a = Vec2::from((1.0, 2.0));
    let b: Vec2 = (3.0, -0.5).into();
    println!("{:?}", a + b);
    println!("{}", a + b == Vec2 { x: 4.0, y: 1.5 });
}

Exercise 3 · quiz

Which derives work?

struct Reading { label: String, value: f64 }. Which of these derives compile? Select all that apply.

Answer
  • Debug

  • Clone

  • PartialEq

  • Default

A derive works only if every field supports the trait. String is not Copy (it owns heap memory), so Copy fails. f64 is PartialEq but not Eq (NaN != NaN breaks the "equal to itself" law), so Eq fails. Debug, Clone, PartialEq and Default work for both field types (Default gives "" and 0.0).