Nugget 6

Home

One Function, Many Types

You wrote fn greet_dog(d: &Dog) and now need fn greet_cat(c: &Cat) — identical except the type. Trait bounds let you write it once.

Starting Point

Let's reuse the Speak trait from the previous nugget:

trait Speak {
    fn make_sound(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Speak for Dog {
    fn make_sound(&self) -> String {
        format!("{} says Woof!", self.name)
    }
}

impl Speak for Cat {
    fn make_sound(&self) -> String {
        format!("{} says Meow!", self.name)
    }
}

The Repetitive Way

Without trait bounds, you'd write a separate function for every type:

fn greet_dog(d: &Dog) {
    println!("Hello! {}", d.make_sound());
}

fn greet_cat(c: &Cat) {
    println!("Hello! {}", c.make_sound());
}

// And another for every new animal...

The Trait Bound Way

Use <T: Speak> to say "for any type T that implements Speak":

fn greet<T: Speak>(animal: &T) {
    println!("Hello! {}", animal.make_sound());
}

fn main() {
    let dog = Dog { name: "Rex".into() };
    let cat = Cat { name: "Whiskers".into() };

    greet(&dog);  // ✅ Hello! Rex says Woof!
    greet(&cat);  // ✅ Hello! Whiskers says Meow!
}

One function works for any type that implements Speak. Add a Parrot tomorrow — greet() works with no changes.

Multiple Trait Bounds

A generic parameter can require multiple traits with the + syntax:

use std::fmt::Debug;

// Requires both Speak and Debug
fn announce<T: Speak + Debug>(animal: &T) {
    dbg!(animal);                    // Debug
    println!("{}", animal.make_sound());  // Speak
}

#[derive(Debug)]
struct Dog { name: String }

impl Speak for Dog { /* as before */ }

fn main() {
    let dog = Dog { name: "Rex".into() };
    announce(&dog);
}

💡 The where clause

For long bounds, the where syntax is cleaner:

fn announce<T>(animal: &T)
where
    T: Speak + Debug + Clone + Display,
{ /* ... */ }

How This Works (Monomorphization)

When you write greet::<Dog>(&dog), the compiler generates a specialized version of greet just for Dog with the method calls resolved at compile time. This is called monomorphization — zero runtime overhead:

// What you write:
fn greet<T: Speak>(animal: &T) { animal.make_sound(); }

// What the compiler generates for Dog:
fn greet__Dog(animal: &Dog) { Dog::make_sound(animal); }

// What the compiler generates for Cat:
fn greet__Cat(animal: &Cat) { Cat::make_sound(animal); }

📐 Zero-cost abstraction

Generic functions with trait bounds have zero runtime overhead compared to writing type-specific functions. The compiler creates a separate copy for each concrete type used, with all method calls resolved at compile time.

Common Built-in Trait Bounds

Bound What it enables Example
T: Debug {:?} formatting fn log<T: Debug>(val: &T)
T: Display {} formatting + .to_string() fn print<T: Display>(val: &T)
T: Clone Explicit .clone() fn duplicate<T: Clone>(val: T) -> T
T: PartialEq == and != fn find<T: PartialEq>(list: &[T], val: &T)
T: Iterator for item in items fn sum<T: Iterator<Item=i32>>(it: T)

Key Takeaways