Nugget 5

Home

Shared Behavior Across Types

You have a Dog and a Cat. Both need to make_sound(). Instead of writing two separate functions, define a trait — the Rust version of an interface.

The Problem

Without traits, you'd write the same logic twice — once for each type:

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

fn dog_sound(d: &Dog) -> String {
    format!("{} says Woof!", d.name)
}

fn cat_sound(c: &Cat) -> String {
    format!("{} says Meow!", c.name)
}

fn main() {
    let dog = Dog { name: "Rex".into() };
    let cat = Cat { name: "Whiskers".into() };
    println!("{}", dog_sound(&dog));
    println!("{}", cat_sound(&cat));
}

This works but doesn't scale. Every new animal requires a new function. Traits let you write it once.

Defining a Trait

A trait declares method signatures without implementations:

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

This says: "Any type implementing Speak must provide a make_sound method that takes a reference to self and returns a String."

Implementing the Trait

Now Dog and Cat both implement Speak, each with their own behavior:

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

trait Speak {
    fn make_sound(&self) -> 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)
    }
}

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

    // Same method name, different behavior
    println!("{}", dog.make_sound());  // Rex says Woof!
    println!("{}", cat.make_sound());  // Whiskers says Meow!
}

📐 This is polymorphism

The same method name (make_sound) triggers different code depending on the type. That's runtime polymorphism without inheritance — traits are Rust's version of interfaces (Java, Go) or type classes (Haskell).

Default Implementations

A trait can provide default method bodies. Implementors can override them or use the default:

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

    // Default implementation — uses make_sound() internally
    fn introduce(&self) -> String {
        format!("I am a pet! {}", self.make_sound())
    }
}

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

impl Speak for Cat {
    fn make_sound(&self) -> String {
        format!("{} says Meow!", self.name)
    }
    // Cat can override the default if it wants:
    // fn introduce(&self) -> String {
    //     "I'm a cat and I do what I want.".to_string()
    // }
}

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

    println!("{}", dog.introduce());  // I am a pet! Rex says Woof!
    println!("{}", cat.introduce());  // I am a pet! Whiskers says Meow!
}

💡 Why default implementations matter

You define the trait once with sensible defaults. New types only implement the minimum required methods and get the rest for free. This is how Display provides .to_string() without you writing it.

Why This Matters

Traits let you:

Key Takeaways