403: Display and Debug Traits
Tutorial Video
Text description (accessibility)
This video demonstrates the "403: Display and Debug Traits" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Formatting output for different audiences requires different representations. Key difference from OCaml: 1. **Automatic derivation**: Rust's `#[derive(Debug)]` generates complete implementations; OCaml's `deriving show` ppx extension provides similar capability.
Tutorial
The Problem
Formatting output for different audiences requires different representations. Debug output should be complete and unambiguous for developers (Color::Rgb(255, 0, 0)); display output should be readable for end users (rgb(255,0,0)). Mixing these leads to either confusing user-facing output or opaque developer output. Rust separates these concerns with two traits: Debug (derivable, for {:?}) and Display (manual, for {}). Additional formatter traits (LowerHex, Binary, Octet, Pointer) handle domain-specific representations.
Display and Debug are the entry points to all of Rust's format machinery — format!, println!, write!, eprintln!, and assert_eq! error messages all rely on them.
🎯 Learning Outcomes
Debug (developer) and Display (user) formattingfmt::Display with fmt::Formatter and write! macro#[derive(Debug)] generates a complete structural representation automaticallyLowerHex for {:x} supportfmt::Formatter provides alignment, width, fill, and precision parametersCode Example
use std::fmt;
#[derive(Debug)] // Auto-generates Debug
enum Color {
Red,
Green,
Blue,
Rgb(u8, u8, u8),
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Color::Red => write!(f, "red"),
Color::Green => write!(f, "green"),
Color::Blue => write!(f, "blue"),
Color::Rgb(r, g, b) => write!(f, "rgb({},{},{})", r, g, b),
}
}
}
fn main() {
let c = Color::Red;
println!("display: {} debug: {:?}", c, c);
}Key Differences
#[derive(Debug)] generates complete implementations; OCaml's deriving show ppx extension provides similar capability.Debug and Display as separate traits; OCaml uses one pp convention, relying on programmer discipline.format!("{}", x) for Display and format!("{:?}", x) for Debug; OCaml uses %a with explicit pp functions.Debug, Display, Binary, Octal, LowerHex, UpperHex, LowerExp, UpperExp, Pointer — a full family; OCaml relies on Printf format strings.OCaml Approach
OCaml has Printf.printf "%s" for string formatting and Format.pp_print_* functions for the Format module's structured pretty-printing. Custom types implement pp : Format.formatter -> t -> unit functions used with %a in format strings. There is no Display/Debug split — the same pp function serves both roles, though libraries conventionally have pp (compact) and show (verbose) variants.
Full Source
#![allow(clippy::all)]
//! Display and Debug Traits
//!
//! Two formatting traits: Debug for developers, Display for users.
use std::fmt;
/// A color type demonstrating Display, Debug, and LowerHex formatting.
#[derive(Debug, Clone, PartialEq)]
pub enum Color {
Red,
Green,
Blue,
Rgb(u8, u8, u8),
}
impl Color {
/// Creates a color from RGB values.
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
Color::Rgb(r, g, b)
}
/// Returns the RGB components.
pub fn to_rgb(&self) -> (u8, u8, u8) {
match self {
Color::Red => (255, 0, 0),
Color::Green => (0, 255, 0),
Color::Blue => (0, 0, 255),
Color::Rgb(r, g, b) => (*r, *g, *b),
}
}
}
/// Display: user-facing output (used by `{}` and `println!`)
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Color::Red => write!(f, "red"),
Color::Green => write!(f, "green"),
Color::Blue => write!(f, "blue"),
Color::Rgb(r, g, b) => write!(f, "rgb({},{},{})", r, g, b),
}
}
}
/// LowerHex: hexadecimal format (used by `{:x}`)
impl fmt::LowerHex for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (r, g, b) = self.to_rgb();
write!(f, "#{:02x}{:02x}{:02x}", r, g, b)
}
}
/// A 2D point demonstrating precision-aware Display.
#[derive(Debug, Clone, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
/// Creates a new point.
pub fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
/// Calculates distance from origin.
pub fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Respect precision from format string if specified
match f.precision() {
Some(p) => write!(f, "({:.prec$}, {:.prec$})", self.x, self.y, prec = p),
None => write!(f, "({:.2}, {:.2})", self.x, self.y),
}
}
}
/// A wrapper that formats its contents in binary.
pub struct Binary<T>(pub T);
impl fmt::Display for Binary<u8> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:08b}", self.0)
}
}
impl fmt::Display for Binary<u16> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:016b}", self.0)
}
}
/// A person struct showing the difference between Debug and Display.
#[derive(Clone)]
pub struct Person {
pub name: String,
pub age: u32,
}
impl Person {
pub fn new(name: &str, age: u32) -> Self {
Person {
name: name.to_string(),
age,
}
}
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({} years old)", self.name, self.age)
}
}
impl fmt::Debug for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Person")
.field("name", &self.name)
.field("age", &self.age)
.finish()
}
}
/// Demonstrates alignment and padding.
pub fn format_aligned(s: &str, width: usize) -> (String, String, String) {
(
format!("{:>width$}", s, width = width), // right-aligned
format!("{:<width$}", s, width = width), // left-aligned
format!("{:^width$}", s, width = width), // centered
)
}
/// Demonstrates numeric formatting.
pub fn format_number(n: u32) -> String {
format!("dec:{} hex:{:#x} oct:{:#o} bin:{:#b}", n, n, n, n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_display() {
assert_eq!(format!("{}", Color::Red), "red");
assert_eq!(format!("{}", Color::Green), "green");
assert_eq!(format!("{}", Color::Blue), "blue");
assert_eq!(format!("{}", Color::Rgb(10, 20, 30)), "rgb(10,20,30)");
}
#[test]
fn test_color_debug() {
assert_eq!(format!("{:?}", Color::Red), "Red");
assert_eq!(format!("{:?}", Color::Rgb(1, 2, 3)), "Rgb(1, 2, 3)");
}
#[test]
fn test_color_hex() {
assert_eq!(format!("{:x}", Color::Red), "#ff0000");
assert_eq!(format!("{:x}", Color::Green), "#00ff00");
assert_eq!(format!("{:x}", Color::Blue), "#0000ff");
assert_eq!(format!("{:x}", Color::Rgb(128, 64, 32)), "#804020");
}
#[test]
fn test_color_to_rgb() {
assert_eq!(Color::Red.to_rgb(), (255, 0, 0));
assert_eq!(Color::rgb(100, 150, 200).to_rgb(), (100, 150, 200));
}
#[test]
fn test_point_display_default() {
let p = Point::new(3.14159, 2.71828);
assert_eq!(format!("{}", p), "(3.14, 2.72)");
}
#[test]
fn test_point_display_precision() {
let p = Point::new(1.23456, 7.89012);
assert_eq!(format!("{:.1}", p), "(1.2, 7.9)");
assert_eq!(format!("{:.4}", p), "(1.2346, 7.8901)");
}
#[test]
fn test_point_debug() {
let p = Point::new(1.0, 2.0);
assert_eq!(format!("{:?}", p), "Point { x: 1.0, y: 2.0 }");
}
#[test]
fn test_binary_display() {
assert_eq!(format!("{}", Binary(5u8)), "00000101");
assert_eq!(format!("{}", Binary(255u8)), "11111111");
}
#[test]
fn test_person_display_vs_debug() {
let p = Person::new("Alice", 30);
assert_eq!(format!("{}", p), "Alice (30 years old)");
assert_eq!(format!("{:?}", p), "Person { name: \"Alice\", age: 30 }");
}
#[test]
fn test_format_aligned() {
let (right, left, center) = format_aligned("hi", 6);
assert_eq!(right, " hi");
assert_eq!(left, "hi ");
assert_eq!(center, " hi ");
}
#[test]
fn test_format_number() {
let s = format_number(255);
assert!(s.contains("dec:255"));
assert!(s.contains("hex:0xff"));
assert!(s.contains("oct:0o377"));
assert!(s.contains("bin:0b11111111"));
}
#[test]
fn test_pretty_debug() {
let p = Point::new(1.0, 2.0);
let pretty = format!("{:#?}", p);
assert!(pretty.contains("Point"));
assert!(pretty.contains("x:"));
assert!(pretty.contains("y:"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_display() {
assert_eq!(format!("{}", Color::Red), "red");
assert_eq!(format!("{}", Color::Green), "green");
assert_eq!(format!("{}", Color::Blue), "blue");
assert_eq!(format!("{}", Color::Rgb(10, 20, 30)), "rgb(10,20,30)");
}
#[test]
fn test_color_debug() {
assert_eq!(format!("{:?}", Color::Red), "Red");
assert_eq!(format!("{:?}", Color::Rgb(1, 2, 3)), "Rgb(1, 2, 3)");
}
#[test]
fn test_color_hex() {
assert_eq!(format!("{:x}", Color::Red), "#ff0000");
assert_eq!(format!("{:x}", Color::Green), "#00ff00");
assert_eq!(format!("{:x}", Color::Blue), "#0000ff");
assert_eq!(format!("{:x}", Color::Rgb(128, 64, 32)), "#804020");
}
#[test]
fn test_color_to_rgb() {
assert_eq!(Color::Red.to_rgb(), (255, 0, 0));
assert_eq!(Color::rgb(100, 150, 200).to_rgb(), (100, 150, 200));
}
#[test]
fn test_point_display_default() {
let p = Point::new(3.14159, 2.71828);
assert_eq!(format!("{}", p), "(3.14, 2.72)");
}
#[test]
fn test_point_display_precision() {
let p = Point::new(1.23456, 7.89012);
assert_eq!(format!("{:.1}", p), "(1.2, 7.9)");
assert_eq!(format!("{:.4}", p), "(1.2346, 7.8901)");
}
#[test]
fn test_point_debug() {
let p = Point::new(1.0, 2.0);
assert_eq!(format!("{:?}", p), "Point { x: 1.0, y: 2.0 }");
}
#[test]
fn test_binary_display() {
assert_eq!(format!("{}", Binary(5u8)), "00000101");
assert_eq!(format!("{}", Binary(255u8)), "11111111");
}
#[test]
fn test_person_display_vs_debug() {
let p = Person::new("Alice", 30);
assert_eq!(format!("{}", p), "Alice (30 years old)");
assert_eq!(format!("{:?}", p), "Person { name: \"Alice\", age: 30 }");
}
#[test]
fn test_format_aligned() {
let (right, left, center) = format_aligned("hi", 6);
assert_eq!(right, " hi");
assert_eq!(left, "hi ");
assert_eq!(center, " hi ");
}
#[test]
fn test_format_number() {
let s = format_number(255);
assert!(s.contains("dec:255"));
assert!(s.contains("hex:0xff"));
assert!(s.contains("oct:0o377"));
assert!(s.contains("bin:0b11111111"));
}
#[test]
fn test_pretty_debug() {
let p = Point::new(1.0, 2.0);
let pretty = format!("{:#?}", p);
assert!(pretty.contains("Point"));
assert!(pretty.contains("x:"));
assert!(pretty.contains("y:"));
}
}
Deep Comparison
OCaml vs Rust: Display and Debug Traits
Side-by-Side Code
OCaml — Manual to_string functions
type color = Red | Green | Blue | Rgb of int * int * int
let string_of_color = function
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
| Rgb (r, g, b) -> Printf.sprintf "rgb(%d,%d,%d)" r g b
let debug_color = function
| Red -> "Color::Red"
| Green -> "Color::Green"
| Blue -> "Color::Blue"
| Rgb (r, g, b) -> Printf.sprintf "Color::Rgb(%d, %d, %d)" r g b
let () =
Printf.printf "display: %s debug: %s\n"
(string_of_color Red) (debug_color Red)
Rust — Traits with automatic integration
use std::fmt;
#[derive(Debug)] // Auto-generates Debug
enum Color {
Red,
Green,
Blue,
Rgb(u8, u8, u8),
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Color::Red => write!(f, "red"),
Color::Green => write!(f, "green"),
Color::Blue => write!(f, "blue"),
Color::Rgb(r, g, b) => write!(f, "rgb({},{},{})", r, g, b),
}
}
}
fn main() {
let c = Color::Red;
println!("display: {} debug: {:?}", c, c);
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| User-facing format | string_of_* function | Display trait, {} placeholder |
| Developer format | Separate debug_* function | Debug trait, {:?} placeholder |
| Auto-derivation | [@@deriving show] (ppx) | #[derive(Debug)] (built-in) |
| Integration | Manual function calls | Automatic via println!, format! |
| Pretty printing | Manual formatting | {:#?} for multi-line |
| Custom formatters | More functions | More traits: LowerHex, Binary, etc. |
Format Specifiers
Rust's formatting system supports many specifiers:
let n = 255;
println!("{}", n); // Display: 255
println!("{:?}", n); // Debug: 255
println!("{:x}", n); // LowerHex: ff
println!("{:X}", n); // UpperHex: FF
println!("{:o}", n); // Octal: 377
println!("{:b}", n); // Binary: 11111111
println!("{:e}", 1.5); // LowerExp: 1.5e0
OCaml uses Printf with different format strings:
Printf.printf "%d %x %o\n" 255 255 255
(* 255 ff 377 *)
The Formatter Parameter
Rust's fmt::Formatter provides formatting options:
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// f.precision() — requested decimal places
// f.width() — requested field width
// f.fill() — padding character
// f.align() — Left, Right, Center
match f.precision() {
Some(p) => write!(f, "({:.prec$}, {:.prec$})", self.x, self.y, prec = p),
None => write!(f, "({:.2}, {:.2})", self.x, self.y),
}
}
}
// Now users can control precision:
println!("{:.4}", point); // 4 decimal places
5 Takeaways
#[derive(Debug)] is free and should be used almost everywhere.** It's a one-liner that enables {:?} printing.
Display is for users, Debug is for developers.** Display should be human-readable; Debug should be useful for debugging.
You write functions and call them. Rust uses trait dispatch.
println! and format!.**No need to call conversion functions — use format specifiers.
LowerHex, Binary, Octal, etc. — implement what makes sense for your type.
Exercises
Display for a Matrix<f64> that formats as aligned columns (use write!(f, "{:8.3}", val) width specifier) and Debug showing the raw flat array.Display for a Tree<i32> (leaf or node with two children) using indentation — leaves on their own line, nodes with children indented 2 spaces. Use fmt::Formatter::write_str for each line.fmt::Binary for a Bitset(u64) type, formatting it as 0b{bits} with the set bits indicated. Implement fmt::Display to show just the set bit positions as {1, 3, 7}.