083 — Display Trait
Tutorial
The Problem
Implement std::fmt::Display for custom types — Color, Point, Person, and a generic Tree<T> — to enable format!, println!, and to_string() without deriving Debug. Compare with OCaml's Printf.sprintf-based to_string functions for the same types.
🎯 Learning Outcomes
fmt::Display using write!(f, "...", ...) in the fmt methodfmt::Formatter as the sink that write! targets{:.1} for floating-point precision inside DisplayDisplay for generic types with T: fmt::Display boundDisplay (user-facing) from Debug (developer-facing)to_string functionsCode Example
#![allow(clippy::all)]
// 083: Display Trait
// Implement Display for custom types
use std::fmt;
// Approach 1: Simple Display
#[derive(Debug)]
enum Color {
Red,
Green,
Blue,
}
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"),
}
}
}
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
// Approach 2: Complex formatting
#[derive(Debug)]
struct Person {
name: String,
age: u32,
email: String,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {}, {})", self.name, self.age, self.email)
}
}
// Approach 3: Recursive Display
#[derive(Debug)]
enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T: fmt::Display> fmt::Display for Tree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Tree::Leaf => write!(f, "."),
Tree::Node(l, v, r) => write!(f, "({} {} {})", l, v, r),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_display() {
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
}
#[test]
fn test_point_display() {
assert_eq!(format!("{}", Point { x: 3.0, y: 4.0 }), "(3.0, 4.0)");
}
#[test]
fn test_person_display() {
let p = Person {
name: "Alice".into(),
age: 30,
email: "alice@ex.com".into(),
};
assert_eq!(format!("{}", p), "Alice (age 30, alice@ex.com)");
}
#[test]
fn test_tree_display() {
let tree = Tree::Node(
Box::new(Tree::Node(Box::new(Tree::Leaf), 1, Box::new(Tree::Leaf))),
2,
Box::new(Tree::Node(Box::new(Tree::Leaf), 3, Box::new(Tree::Leaf))),
);
assert_eq!(format!("{}", tree), "((. 1 .) 2 (. 3 .))");
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Interface | impl fmt::Display trait | to_string function per type |
| Generic elements | T: fmt::Display bound | to_s : 'a -> string parameter |
| Debug format | #[derive(Debug)] | Printf.sprintf "%S" / ppx |
to_string() | Auto from Display | Explicit function |
| Sink type | fmt::Formatter | Returns string directly |
| Format spec | {:.1}, {:>10}, etc. | %.1f, %10s, etc. |
Rust's Display trait integrates with the entire format!/println!/write! machinery. Any type that implements Display can be used in any format string {} position. OCaml's approach is more direct but requires explicitly threading the to_string function through generic code.
OCaml Approach
OCaml does not have a single Display trait; each type gets its own to_string function. Printf.sprintf "(%.1f, %.1f)" p.x p.y formats a point. For recursive tree, a higher-order tree_to_string to_s takes the element formatter as an argument, since OCaml has no trait-bound system. The result is the same string — the mechanism is different: explicit function passing vs trait dispatch.
Full Source
#![allow(clippy::all)]
// 083: Display Trait
// Implement Display for custom types
use std::fmt;
// Approach 1: Simple Display
#[derive(Debug)]
enum Color {
Red,
Green,
Blue,
}
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"),
}
}
}
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
// Approach 2: Complex formatting
#[derive(Debug)]
struct Person {
name: String,
age: u32,
email: String,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {}, {})", self.name, self.age, self.email)
}
}
// Approach 3: Recursive Display
#[derive(Debug)]
enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T: fmt::Display> fmt::Display for Tree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Tree::Leaf => write!(f, "."),
Tree::Node(l, v, r) => write!(f, "({} {} {})", l, v, r),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_display() {
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
}
#[test]
fn test_point_display() {
assert_eq!(format!("{}", Point { x: 3.0, y: 4.0 }), "(3.0, 4.0)");
}
#[test]
fn test_person_display() {
let p = Person {
name: "Alice".into(),
age: 30,
email: "alice@ex.com".into(),
};
assert_eq!(format!("{}", p), "Alice (age 30, alice@ex.com)");
}
#[test]
fn test_tree_display() {
let tree = Tree::Node(
Box::new(Tree::Node(Box::new(Tree::Leaf), 1, Box::new(Tree::Leaf))),
2,
Box::new(Tree::Node(Box::new(Tree::Leaf), 3, Box::new(Tree::Leaf))),
);
assert_eq!(format!("{}", tree), "((. 1 .) 2 (. 3 .))");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_display() {
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
}
#[test]
fn test_point_display() {
assert_eq!(format!("{}", Point { x: 3.0, y: 4.0 }), "(3.0, 4.0)");
}
#[test]
fn test_person_display() {
let p = Person {
name: "Alice".into(),
age: 30,
email: "alice@ex.com".into(),
};
assert_eq!(format!("{}", p), "Alice (age 30, alice@ex.com)");
}
#[test]
fn test_tree_display() {
let tree = Tree::Node(
Box::new(Tree::Node(Box::new(Tree::Leaf), 1, Box::new(Tree::Leaf))),
2,
Box::new(Tree::Node(Box::new(Tree::Leaf), 3, Box::new(Tree::Leaf))),
);
assert_eq!(format!("{}", tree), "((. 1 .) 2 (. 3 .))");
}
}
Deep Comparison
Core Insight
Display controls how a type is printed with {}. Unlike Debug (derived), Display must be manually implemented — it's the user-facing representation.
OCaml Approach
to_string function manuallyPrintf.sprintf with format stringsRust Approach
impl fmt::Display for Typeformat!("{}", x), println!("{}", x)fmt method returns fmt::ResultComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Protocol | Manual to_string | impl Display |
| Format string | %s with to_string | {} automatic |
| Debug | #show (ppx) | #[derive(Debug)] |
| Derive | No | Display: no, Debug: yes |
Exercises
fmt::Display for a Matrix(Vec<Vec<f64>>) newtype that prints rows separated by newlines.impl fmt::Display for Tree<T> variant that prints in indented format (each level adds two spaces).fmt::Debug manually for Person to show Person { name: "Alice", age: 30, email: "…" }.Wrapper<T: Display>(Vec<T>) that displays its items as [a, b, c].Printable module type with val to_string : t -> string and a functor PrintList(P: Printable) with val print_list : P.t list -> string.