DaleSchool

Traits: Certificates for Types

Intermediate20min

Learning Objectives

  • Define a common interface with trait
  • Implement a trait on a struct
  • Use derive macros to auto-implement traits
  • Understand the concept of trait bounds

Working Code

In Lesson 12 you put #[derive(Debug)] on structs. Back then, the explanation was "add this to print with {:?}." Now it's time to reveal what it really is — a trait!

struct Cat {
    name: String,
    age: u8,
}

struct Dog {
    name: String,
    age: u8,
}

trait Greet {
    fn hello(&self) -> String;
}

impl Greet for Cat {
    fn hello(&self) -> String {
        format!("{} says meow!", self.name)
    }
}

impl Greet for Dog {
    fn hello(&self) -> String {
        format!("{} says woof!", self.name)
    }
}

fn main() {
    let cat = Cat { name: "Whiskers".into(), age: 3 };
    let dog = Dog { name: "Buddy".into(), age: 5 };

    println!("{}", cat.hello());
    println!("{}", dog.hello());
}

Greet is a trait. It means "implement this method and you earn the ability to greet." Cat and Dog greet differently, but both have the Greet certificate!

Try It Yourself

  1. Create a Bird struct and implement the Greet trait for it.
  2. Call hello() and verify the result.
  3. Add a goodbye(&self) -> String method to the Greet trait. What happens? (Hint: every implementor now shows an error!)

"Why?" — The Certificate Analogy

Think of traits as certificates:

  • Display certificate -> can use println!("{}", x)
  • Debug certificate -> can use println!("{:?}", x)
  • Clone certificate -> can use .clone()
  • PartialEq certificate -> can compare with ==

No certificate? The compiler tells you: "This type doesn't have the X certificate."

Display — Show It to Users

To print with {}, you need the Display trait. It can't be auto-generated, so you implement it manually:

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("Coordinates: {p}");     // Coordinates: (3, 4)
    // println!("{:?}", p);           // error! no Debug
}

Debug — For Developers

To print with {:?}, you need the Debug trait. It can be auto-generated with derive:

#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{:?}", p);   // Point { x: 3.0, y: 4.0 }
    println!("{:#?}", p);  // pretty-printed
}

Before / After — With and Without Display

Before — no Display

struct Temperature {
    celsius: f64,
}

fn main() {
    let t = Temperature { celsius: 36.5 };
    // println!("{t}"); // error! `Temperature` doesn't implement `Display`
    println!("Temp: {}°C", t.celsius); // must access the field directly
}

After — Display implemented

use std::fmt;

struct Temperature {
    celsius: f64,
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°C", self.celsius)
    }
}

fn main() {
    let t = Temperature { celsius: 36.5 };
    println!("Current temp: {t}"); // Current temp: 36.5°C
}

derive Macros — Auto-Certificates

Some traits can be auto-generated with #[derive(...)]. You already used this in Lesson 12!

| Trait | Ability | Example | | ----------- | ------------------------- | --------------------- | | Debug | Print with {:?} | println!("{:?}", x) | | Clone | Duplicate with .clone() | let y = x.clone() | | PartialEq | Compare with == | if a == b | | Default | Create default values | Point::default() |

#[derive(Debug, Clone, PartialEq)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

fn main() {
    let red = Color { r: 255, g: 0, b: 0 };
    let red2 = red.clone();     // possible thanks to Clone

    println!("{:?}", red);      // possible thanks to Debug
    println!("{}", red == red2); // possible thanks to PartialEq -> true
}

Why could you use .clone() back in Phase 1? That was the Clone trait at work!

Trait Bounds — "Only Types With This Certificate"

You can restrict a function to accept only types that implement certain traits:

use std::fmt::Display;

fn print_twice<T: Display>(item: T) {
    println!("1st: {item}");
    println!("2nd: {item}");
}

fn main() {
    print_twice("hello");
    print_twice(42);
    // print_twice(vec![1, 2, 3]); // error! Vec doesn't implement Display
}

T: Display means "T must have the Display certificate." Generics and trait bounds are covered further in the next lesson.

Default Implementations — Preset Behavior

You can provide a default implementation in a trait. Types only override it when they need to:

trait Describable {
    fn name(&self) -> &str;

    // Default implementation — used if not overridden
    fn describe(&self) -> String {
        format!("Name: {}", self.name())
    }
}

struct Product {
    name: String,
    price: u32,
}

impl Describable for Product {
    fn name(&self) -> &str {
        &self.name
    }

    // describe() is not overridden, so the default is used
}

struct PremiumProduct {
    name: String,
    price: u32,
}

impl Describable for PremiumProduct {
    fn name(&self) -> &str {
        &self.name
    }

    // Override the default implementation!
    fn describe(&self) -> String {
        format!("* {} (${}.00)", self.name(), self.price)
    }
}

fn main() {
    let apple = Product { name: "Apple".into(), price: 1 };
    let gold_apple = PremiumProduct { name: "Gold Apple".into(), price: 50 };

    println!("{}", apple.describe());      // Name: Apple
    println!("{}", gold_apple.describe()); // * Gold Apple ($50.00)
}

Exercise 1 (easy). Implement the Display trait for the Rectangle struct so it prints as "3 x 5 rectangle".

use std::fmt;

struct Rectangle {
    width: u32,
    height: u32,
}

// Implement Display here

fn main() {
    let rect = Rectangle { width: 3, height: 5 };
    println!("{rect}"); // "3 x 5 rectangle"
}

Exercise 2 (medium). Define a Summary trait and implement it for Article and Tweet.

// Define the Summary trait
// - Requires a summarize(&self) -> String method

struct Article {
    title: String,
    content: String,
}

struct Tweet {
    username: String,
    text: String,
}

// Implement Summary for each struct
// Article: "{title} — {first 20 chars of content}..."
// Tweet: "@{username}: {text}"

fn main() {
    let article = Article {
        title: "Rust 2024".into(),
        content: "Rust has been voted the most loved language again this year.".into(),
    };
    let tweet = Tweet {
        username: "rustlang".into(),
        text: "Rust 1.80 released!".into(),
    };

    println!("{}", article.summarize());
    println!("{}", tweet.summarize());
}

Exercise 3 (challenge). Use trait bounds to write a print_max function that accepts types implementing both Display and PartialOrd.

use std::fmt::Display;

fn print_max<T: Display + PartialOrd>(a: T, b: T) {
    // Print the larger of a and b
    // Hint: if a >= b { ... } else { ... }
}

fn main() {
    print_max(10, 20);           // Max: 20
    print_max("apple", "banana"); // Max: banana
}

Q1. What is a trait?

  • A) A way to define struct fields
  • B) A way to define a common interface that types must implement
  • C) A way to restrict function return types
  • D) A way to specify variable lifetimes

Q2. What does #[derive(Debug, Clone)] do?

  • A) Automatically adds fields to the struct
  • B) Automatically implements the Debug and Clone traits
  • C) Deletes the struct
  • D) Makes the struct generic

Q3. Why does this code produce an error?

struct Point { x: f64, y: f64 }

fn main() {
    let p = Point { x: 1.0, y: 2.0 };
    println!("{p}");
}
  • A) Point doesn't have the Debug trait
  • B) Point doesn't have the Display trait
  • C) You can't pass a struct to println!
  • D) f64 can't be printed