Activity 23 - Traits

Overview

In this activity, you'll practice defining and implementing traits. We'll start with one example together, then you'll implement your own trait in the Rust Playground.

Part 1: Live-coding example - Summary Trait

We'll implement a Summary trait that works for different types.

// 1. Define the trait
trait Summary {
    fn summarize(&self) -> String;

    // How long will this take to consume?
    fn time_needed(&self) -> u32;  // in minutes

    // Default implementation using time_needed
    fn commitment_level(&self) -> String {
        let time = self.time_needed();
        if time < 60 {
            format!("Quick! Just {} minutes", time)
        } else if time < 180 {
            format!("Moderate commitment: {:.1} hours", time as f64 / 60.0)
        } else {
            format!("Big commitment: {:.1} hours", time as f64 / 60.0)
        }
    }
}

// 2. Define some types
struct Book {
    title: String,
    author: String,
    pages: u32,
}

struct Movie {
    title: String,
    director: String,
    runtime_minutes: u32,
}

// 3. Implement Summary for Book
impl Summary for Book {
    // TODO
}

// 4. Implement Summary for Movie
impl Summary for Movie {
    // TODO
}

// 5. Use the trait in a function
fn print_info(item: &impl Summary) {
    // TODO
}

fn main() {
    let book = Book {
        title: "The Rust Programming Language".to_string(),
        author: "Steve Klabnik".to_string(),
        pages: 560,
    };

    let movie = Movie {
        title: "The Matrix".to_string(),
        director: "Wachowskis".to_string(),
        runtime_minutes: 136,
    };

    print_info(&book);
    println!();
    print_info(&movie);
}

Part 2: Your Turn - Measurable and Shape2D Traits

Learning goal: Implementing traits and understanding trait extension

You'll implement two related traits: Measurable (for anything with a size) and Shape2D (for 2D shapes, which extends Measurable).

Step 1: Define the structs and traits you'll need

Paste this into Rust Playground:

#![allow(unused)]
fn main() {
trait Measurable {
    fn size(&self) -> f64;

    fn size_category(&self) -> String {
        // Default implementation
        if self.size() < 10.0 {
            "Small".to_string()
        } else if self.size() < 100.0 {
            "Medium".to_string()
        } else {
            "Large".to_string()
        }
    }
}

// Shape2D extends Measurable - any Shape2D must also implement Measurable!
trait Shape2D: Measurable {
    fn bounding_width(&self) -> f64;   // Width of bounding box
    fn bounding_height(&self) -> f64;  // Height of bounding box

    // Default: area is the same as size
    fn area(&self) -> f64 {
        self.size()
    }

    fn bounding_area(&self) -> f64 {
        self.bounding_width() * self.bounding_height()
    }

    // This gives a fraction of the space used by the shape if they were to be packed in a grid
    fn packing_efficiency(&self) -> f64 {
        self.area() / self.bounding_area()
    }
}

#[derive(Debug)]
struct Rectangle {
    width: f64,
    height: f64,
}

#[derive(Debug)]
struct Circle {
    radius: f64,
}

#[derive(Debug)]
struct CrayonBox {
    count: u32,    
    price: f64,  
    barcode: String,
}
}

Step 2: Implement Measurable for each struct

Add these templates to your rust playground code and complete them - this step is done when the code in main runs successfully!

impl Measurable for Rectangle {
    fn size(&self) -> f64 {
        // Return the area
        // Your code here
    }
}

impl Measurable for Circle {
    fn size(&self) -> f64 {
        // Return the area (π × radius²)
        // Hint: use std::f64::consts::PI
        // Your code here
    }
}

impl Measurable for CrayonBox {
    fn size(&self) -> f64 {
        // Size is just the count of crayons
        // But be careful of types!
        // Your code here
    }

    // Override size_category for different thresholds
    // You don't need to modify this
    fn size_category(&self) -> String {
        if self.count <= 8 {
            "Small box".to_string()
        } else if self.count <= 24 {
            "Medium box".to_string()
        } else {
            "Large box".to_string()
        }
    }
}

fn main() {
    let rect = Rectangle { width: 5.0, height: 3.0 };
    let circle = Circle { radius: 2.0 };
    let crayons = CrayonBox {
        count: 64,
        price: 12.99,
        barcode: "071662078645".to_string(),
    };

    println!("Rectangle:");
    println!("Size: {}", rect.size());
    println!("{}", rect.size_category());

    println!("\nCircle:");
    println!("Size: {}", circle.size());
    println!("{}", circle.size_category());
    
    println!("\nCrayons:");
    println!("Size: {}", crayons.size());
    println!("{}", crayons.size_category());
}

Step 4: Implement Shape2D for Rectangle and Circle only

Add this code to your playground, and REPLACE the code in main:

impl Shape2D for Rectangle {
    fn bounding_width(&self) -> f64 {
        // Your code here
    }

    fn bounding_height(&self) -> f64 {
        // Your code here
    }

    // area() uses the default implementation (calls self.size())
}

impl Shape2D for Circle {
    fn bounding_width(&self) -> f64 {
        // Width of bounding box
        // Your code here
    }

    fn bounding_height(&self) -> f64 {
        // Height of bounding box
        // Your code here
    }
}

fn main() {
    let rect = Rectangle { width: 5.0, height: 3.0 };
    let circle = Circle { radius: 2.0 };

    println!("Rectangle:");
    println!("{}}", rect.packing_efficiency());

    println!("\nCircle:");
    describe_shape(&circle);
    println!("{}}", circle.packing_efficiency());
    
    let crayons = CrayonBox {
        count: 64,
        price: 12.99,
        barcode: "071662078645".to_string(),
    };

    // CrayonBox is not Shape2D so we can just just do:
    println!("\nCrayons:");
    println!("Size: {}", crayons.size());
}

Challenge / Extension: Implementing PartialOrd

Make shapes comparable by area! Implement PartialOrd for Rectangle so you can compare which shape is bigger.

use std::cmp::Ordering;

// First, you need PartialEq (required for PartialOrd)
impl PartialEq for Rectangle {
    fn eq(&self, other: &Rectangle) -> bool {
        // Your code here
    }
}

// Now implement PartialOrd - compare by area
impl PartialOrd for Rectangle {
    fn partial_cmp(&self, other: &Rectangle) -> Option<Ordering> {
        // Compare areas
        // Hint: f64 already implements partial_cmp
        // So if x and y are f64 values
        // You could return x.partial_cmp(y)
        // Your code here
    }
}

// Now you can write a function that computes the area of the larger shape
fn larger_area<T>(shape1: &T, shape2: &T) -> f64
where
    T: Shape2D + PartialOrd
{
    // Use > to compare shape1 and shape2, return the area of the larger one
    // Your code here
}

fn main() {
    let rect1 = Rectangle { width: 5.0, height: 3.0 };   // area = 15
    let rect2 = Rectangle { width: 4.0, height: 4.0 };   // area = 16

    let bigger_area = larger_area(&rect1, &rect2);
    println!("Bigger rectangle has area: {:.2}", bigger_area);  // 16.00

    // Test some comparisons
    println!("rect1 < rect2: {}", rect1 < rect2);  // true
    println!("rect1 == rect2: {}", rect1 == rect2);  // false
}