Deriving and Implementing Traits: Debug and Display

About This Module

This module explores the #[derive(Debug)] attribute in detail and introduces manual trait implementations. Understanding how derive macros work and when to implement traits manually is important for creating user-friendly and debuggable Rust programs.

Prework

Prework Readings

Read the following sections from "The Rust Programming Language" book:

Pre-lecture Reflections

Before class, consider these questions:

  1. What is the difference between Debug and Display formatting?
  2. Why might you want to manually implement a trait instead of using derive?
  3. How do derive macros help reduce boilerplate code?
  4. When would custom formatting be important for user experience?
  5. What role do traits play in Rust's type system?

Lecture

Learning Objectives

By the end of this module, you should be able to:

  • Understand what #[derive(Debug)] does under the hood
  • Manually implement the Debug trait for custom types
  • Implement the Display trait for user-friendly output
  • Choose between Debug and Display formatting appropriately
  • Understand when to use derive vs. manual implementation
  • Apply formatting traits to make code more debuggable

Understanding #[derive(Debug)]

  • A simple way to tell Rust to generate code that allows a complex type to be printed
  • Here's the equivalent manual implementation of the Debug trait
  • more on traits and impl later
use std::fmt;

impl fmt::Debug for Direction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
           match *self {
               Direction::North => write!(f, "North"),
               Direction::East => write!(f, "East"),
               Direction::South => write!(f, "South"),
               Direction::West => write!(f, "West"),               
           }
    }
}
let dir = Direction::North;
dir
North
let dir = Direction::North;
println!("{:?}",dir);
North
// Example of how make a complex datatype printable directly (without deriving from Debug)
use std::fmt;

impl fmt::Display for Direction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
           match *self {
               Direction::North => write!(f, "North"),
               Direction::East => write!(f, "East"),
               Direction::South => write!(f, "South"),
               Direction::West => write!(f, "West"),               
           }
    }
}
println!("{}", dir);
North

Best Practices

Design Guidelines:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
pub enum UserAction {
    Login(String),
    Logout,
    UpdateProfile { name: String, email: String },
}

impl fmt::Display for UserAction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            UserAction::Login(username) => write!(f, "User '{}' logged in", username),
            UserAction::Logout => write!(f, "User logged out"),
            UserAction::UpdateProfile { name, .. } => {
                write!(f, "User '{}' updated profile", name)
            }
        }
    }
}

// Usage
let action = UserAction::Login("alice".to_string());
println!("{}", action);     // User-friendly: "User 'alice' logged in"
println!("{:?}", action);   // Debug: Login("alice")
}

When to Implement Manually:

  • Security: Hide sensitive information in debug output
  • Performance: Optimize formatting for frequently-used types
  • User Experience: Create polished output for end users
  • Domain Requirements: Follow domain-specific formatting conventions