Level 3 · Power Tools

Traits & Generics 🧬

Welcome to Level 3. First tool: writing code that works for many types at once, without losing an ounce of safety or speed.

Traits: shared abilities

A trait describes something types can do. Think of it as a badge 🎖️: "this type can be summarized," "this type can be compared," "this type can be printed." You define the badge; any type can earn it:

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

    // traits can include default behavior, too
    fn shout(&self) -> String {
        self.describe().to_uppercase() + "!!!"
    }
}

struct Crab { name: String }
struct Wave { height_m: f64 }

impl Describe for Crab {
    fn describe(&self) -> String {
        format!("a crab named {}", self.name)
    }
}

impl Describe for Wave {
    fn describe(&self) -> String {
        format!("a {}m wave", self.height_m)
    }
}

fn main() {
    let rusty = Crab { name: String::from("Rusty") };
    let big = Wave { height_m: 3.2 };
    println!("{}", rusty.describe());
    println!("{}", big.shout());   // default method, free of charge
}

Two completely unrelated types now share a vocabulary. If a type skips a required method, the compiler refuses the badge: a half-implemented interface can't exist.

Functions that accept "anything with the badge"

fn announce(item: &impl Describe) {
    println!("📣 Behold: {}", item.describe());
}

fn main() {
    announce(&Crab { name: String::from("Rusty") });
    announce(&Wave { height_m: 1.5 });
}

&impl Describe reads exactly as it sounds: "a borrow of anything that implements Describe." One function, every describable type, forever, including types other people write next year.

Generics: type placeholders

Sometimes the flexibility isn't about behavior but about containers. You've been using generics all along: Vec<T>, Option<T>, Result<T, E>. The T is a placeholder filled in per use: Vec<i32>, Option<String>. You can write your own:

#[derive(Debug)]
struct Pair<T> {
    first: T,
    second: T,
}

fn main() {
    let numbers = Pair { first: 1, second: 2 };          // Pair<i32>
    let words = Pair { first: "sea", second: "shell" };  // Pair<&str>
    println!("{numbers:?} {words:?}");
}

Trait bounds: placeholders with standards

Now combine them. A generic function can demand its placeholder have a badge. This is the pattern that unlocks most of Rust's standard library:

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut biggest = &list[0];
    for item in list {
        if item > biggest {     // > only works because T: PartialOrd
            biggest = item;
        }
    }
    biggest
}

fn main() {
    println!("{}", largest(&[3, 7, 2]));            // works on numbers
    println!("{}", largest(&['x', 'a', 'm']));      // and chars
    println!("{}", largest(&["kelp", "algae"]));    // and strings
}

T: PartialOrd means "any type T that can be compared with < and >." Remove the bound and the compiler rejects the >: it won't let you assume abilities a type might not have. Maximum flexibility, zero leaps of faith.

⚡ The free lunch Here's the magic: at compile time Rust generates a specialized copy of largest for each type you use it with, as if you'd hand-written an i32 version and a char version. Generic code runs exactly as fast as duplicated code. This is called zero-cost abstraction, and it's a core reason Rust wins benchmarks.

Badges you've already met

TraitGrantsUsually earned by
Debugprinting with {:?}#[derive(Debug)]
Clone.clone()#[derive(Clone)]
Copycopy instead of move (Lesson 6!)#[derive(Copy, Clone)], small types only
PartialEq== and !=#[derive(PartialEq)]
PartialOrd< > comparisons#[derive(PartialOrd)]
Displayprinting with {}written by hand; you choose the look

That mysterious #[derive(...)] from Lesson 8 was trait implementation all along: the compiler writing badge paperwork for you.

⚠️ Common stumbles
  • Using >, == or {:?} on a generic T without the bound that grants it.
  • Forgetting for in impl Describe for Crab.
  • Deriving Copy on heap-owning types like String: not allowed, and the compiler explains why.
Exercise 1

Noise-makers

Define a trait MakesSound with fn sound(&self) -> String. Implement it for structs Dog and Robot, then write fn broadcast(thing: &impl MakesSound) that prints the sound three times.

Reveal solution
trait MakesSound {
    fn sound(&self) -> String;
}

struct Dog;
struct Robot;

impl MakesSound for Dog {
    fn sound(&self) -> String { String::from("woof") }
}

impl MakesSound for Robot {
    fn sound(&self) -> String { String::from("beep-boop") }
}

fn broadcast(thing: &impl MakesSound) {
    let s = thing.sound();
    println!("{s} {s} {s}!");
}

fn main() {
    broadcast(&Dog);
    broadcast(&Robot);
}
Exercise 2

Generic swap

Write fn swap<T>(pair: (T, T)) -> (T, T) that returns the tuple reversed. Test it with numbers and with &strs. Did you need any trait bound? Why not?

Reveal solution
fn swap<T>(pair: (T, T)) -> (T, T) {
    (pair.1, pair.0)
}

fn main() {
    println!("{:?}", swap((1, 2)));          // (2, 1)
    println!("{:?}", swap(("hi", "yo")));    // ("yo", "hi")
}

No bound needed: swapping only moves values around. Bounds are for when you use abilities: compare, print, clone…

Exercise 3

A proper Display

Give Lesson 8's Player the Display badge by hand, so println!("{p}") prints CrabLord99 (level 7). Skeleton:

use std::fmt;

impl fmt::Display for Player {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "...")   // your format here
    }
}
Reveal solution
use std::fmt;

struct Player {
    name: String,
    level: u32,
}

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

fn main() {
    let p = Player { name: String::from("CrabLord99"), level: 7 };
    println!("{p}");   // CrabLord99 (level 7)
}