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
//! 083: Display Trait
//!
//! `std::fmt::Display` is how a type declares its canonical, user-facing
//! string form: the format produced by `{}` and `.to_string()`. It is the
//! Rust counterpart to OCaml's ad-hoc `foo_to_string` helpers, with the
//! important difference that it is a trait — any generic code can ask for
//! `T: Display` and get a uniform way to render the value.
//!
//! `Debug` (the `{:?}` formatter) is the developer-facing sibling: it
//! should be unambiguous and is typically derived. `Display` is opinionated
//! about presentation and is written by hand.
//!
//! This module demonstrates three idioms:
//!
//! * a simple enum rendered via `Display`,
//! * a record with both a presentational `Display` and a manual `Debug`,
//! * a recursive generic data type whose `Display` impl delegates to its
//! element type's `Display`.
use std::fmt;
// ---------------------------------------------------------------------------
// Approach 1: Simple `Display` on an enum and a record
// ---------------------------------------------------------------------------
/// A primary color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Red,
Green,
Blue,
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Color::Red => "Red",
Color::Green => "Green",
Color::Blue => "Blue",
};
f.write_str(name)
}
}
/// A 2-D point rendered with one decimal of precision per coordinate.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
/// Creates a new `Point` at `(x, y)`.
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
// ---------------------------------------------------------------------------
// Approach 2: Distinct `Display` and `Debug` for the same type
// ---------------------------------------------------------------------------
/// A person with a presentational `Display` form and a hand-written
/// `Debug` form that quotes string fields (mirroring OCaml's `%S`).
#[derive(Clone, PartialEq, Eq)]
pub struct Person {
pub name: String,
pub age: u32,
pub email: String,
}
impl Person {
/// Creates a new `Person`.
pub fn new(name: impl Into<String>, age: u32, email: impl Into<String>) -> Self {
Self {
name: name.into(),
age,
email: email.into(),
}
}
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {}, {})", self.name, self.age, self.email)
}
}
impl fmt::Debug for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{ name = {:?}; age = {}; email = {:?} }}",
self.name, self.age, self.email
)
}
}
// ---------------------------------------------------------------------------
// Approach 3: Recursive `Display` on a generic tree
// ---------------------------------------------------------------------------
/// A binary tree whose nodes carry a payload of type `T`.
///
/// `Leaf` is the empty tree; `Node(left, value, right)` is an internal
/// node. A blanket `Display` impl delegates formatting of `T` to its own
/// `Display` instance, yielding Lisp-style parenthesized output such as
/// `((. 1 .) 2 (. 3 .))`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T> Tree<T> {
/// A shorthand for the empty tree.
pub fn leaf() -> Self {
Tree::Leaf
}
/// Constructs an internal node from owned subtrees.
pub fn node(left: Tree<T>, value: T, right: Tree<T>) -> Self {
Tree::Node(Box::new(left), value, Box::new(right))
}
}
impl<T: fmt::Display> fmt::Display for Tree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Tree::Leaf => f.write_str("."),
Tree::Node(left, value, right) => write!(f, "({left} {value} {right})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1 ---------------------------------------------------------
#[test]
fn color_display_matches_variant_name() {
assert_eq!(Color::Red.to_string(), "Red");
assert_eq!(Color::Green.to_string(), "Green");
assert_eq!(Color::Blue.to_string(), "Blue");
}
#[test]
fn color_display_and_format_macro_agree() {
assert_eq!(format!("{}", Color::Red), "Red");
}
#[test]
fn point_display_uses_one_decimal() {
let p = Point::new(3.0, 4.0);
assert_eq!(p.to_string(), "(3.0, 4.0)");
}
#[test]
fn point_display_rounds_to_one_decimal() {
let p = Point::new(1.23, -2.78);
assert_eq!(p.to_string(), "(1.2, -2.8)");
}
// --- Approach 2 ---------------------------------------------------------
#[test]
fn person_display_is_presentational() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(p.to_string(), "Alice (age 30, alice@ex.com)");
}
#[test]
fn person_debug_quotes_string_fields() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(
format!("{p:?}"),
"{ name = \"Alice\"; age = 30; email = \"alice@ex.com\" }"
);
}
// --- Approach 3 ---------------------------------------------------------
#[test]
fn empty_tree_renders_as_dot() {
let t: Tree<i32> = Tree::leaf();
assert_eq!(t.to_string(), ".");
}
#[test]
fn tree_of_integers_renders_recursively() {
let t = Tree::node(
Tree::node(Tree::leaf(), 1, Tree::leaf()),
2,
Tree::node(Tree::leaf(), 3, Tree::leaf()),
);
assert_eq!(t.to_string(), "((. 1 .) 2 (. 3 .))");
}
#[test]
fn tree_is_generic_over_any_display_payload() {
let t = Tree::node(Tree::leaf(), "hi", Tree::leaf());
assert_eq!(t.to_string(), "(. hi .)");
}
#[test]
fn tree_display_composes_with_nested_display_types() {
let t = Tree::node(
Tree::node(Tree::leaf(), Color::Red, Tree::leaf()),
Color::Green,
Tree::leaf(),
);
assert_eq!(t.to_string(), "((. Red .) Green .)");
}
}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
//! 083: Display Trait
//!
//! `std::fmt::Display` is how a type declares its canonical, user-facing
//! string form: the format produced by `{}` and `.to_string()`. It is the
//! Rust counterpart to OCaml's ad-hoc `foo_to_string` helpers, with the
//! important difference that it is a trait — any generic code can ask for
//! `T: Display` and get a uniform way to render the value.
//!
//! `Debug` (the `{:?}` formatter) is the developer-facing sibling: it
//! should be unambiguous and is typically derived. `Display` is opinionated
//! about presentation and is written by hand.
//!
//! This module demonstrates three idioms:
//!
//! * a simple enum rendered via `Display`,
//! * a record with both a presentational `Display` and a manual `Debug`,
//! * a recursive generic data type whose `Display` impl delegates to its
//! element type's `Display`.
use std::fmt;
// ---------------------------------------------------------------------------
// Approach 1: Simple `Display` on an enum and a record
// ---------------------------------------------------------------------------
/// A primary color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Color {
Red,
Green,
Blue,
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Color::Red => "Red",
Color::Green => "Green",
Color::Blue => "Blue",
};
f.write_str(name)
}
}
/// A 2-D point rendered with one decimal of precision per coordinate.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
/// Creates a new `Point` at `(x, y)`.
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({:.1}, {:.1})", self.x, self.y)
}
}
// ---------------------------------------------------------------------------
// Approach 2: Distinct `Display` and `Debug` for the same type
// ---------------------------------------------------------------------------
/// A person with a presentational `Display` form and a hand-written
/// `Debug` form that quotes string fields (mirroring OCaml's `%S`).
#[derive(Clone, PartialEq, Eq)]
pub struct Person {
pub name: String,
pub age: u32,
pub email: String,
}
impl Person {
/// Creates a new `Person`.
pub fn new(name: impl Into<String>, age: u32, email: impl Into<String>) -> Self {
Self {
name: name.into(),
age,
email: email.into(),
}
}
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {}, {})", self.name, self.age, self.email)
}
}
impl fmt::Debug for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{ name = {:?}; age = {}; email = {:?} }}",
self.name, self.age, self.email
)
}
}
// ---------------------------------------------------------------------------
// Approach 3: Recursive `Display` on a generic tree
// ---------------------------------------------------------------------------
/// A binary tree whose nodes carry a payload of type `T`.
///
/// `Leaf` is the empty tree; `Node(left, value, right)` is an internal
/// node. A blanket `Display` impl delegates formatting of `T` to its own
/// `Display` instance, yielding Lisp-style parenthesized output such as
/// `((. 1 .) 2 (. 3 .))`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Tree<T> {
Leaf,
Node(Box<Tree<T>>, T, Box<Tree<T>>),
}
impl<T> Tree<T> {
/// A shorthand for the empty tree.
pub fn leaf() -> Self {
Tree::Leaf
}
/// Constructs an internal node from owned subtrees.
pub fn node(left: Tree<T>, value: T, right: Tree<T>) -> Self {
Tree::Node(Box::new(left), value, Box::new(right))
}
}
impl<T: fmt::Display> fmt::Display for Tree<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Tree::Leaf => f.write_str("."),
Tree::Node(left, value, right) => write!(f, "({left} {value} {right})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1 ---------------------------------------------------------
#[test]
fn color_display_matches_variant_name() {
assert_eq!(Color::Red.to_string(), "Red");
assert_eq!(Color::Green.to_string(), "Green");
assert_eq!(Color::Blue.to_string(), "Blue");
}
#[test]
fn color_display_and_format_macro_agree() {
assert_eq!(format!("{}", Color::Red), "Red");
}
#[test]
fn point_display_uses_one_decimal() {
let p = Point::new(3.0, 4.0);
assert_eq!(p.to_string(), "(3.0, 4.0)");
}
#[test]
fn point_display_rounds_to_one_decimal() {
let p = Point::new(1.23, -2.78);
assert_eq!(p.to_string(), "(1.2, -2.8)");
}
// --- Approach 2 ---------------------------------------------------------
#[test]
fn person_display_is_presentational() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(p.to_string(), "Alice (age 30, alice@ex.com)");
}
#[test]
fn person_debug_quotes_string_fields() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(
format!("{p:?}"),
"{ name = \"Alice\"; age = 30; email = \"alice@ex.com\" }"
);
}
// --- Approach 3 ---------------------------------------------------------
#[test]
fn empty_tree_renders_as_dot() {
let t: Tree<i32> = Tree::leaf();
assert_eq!(t.to_string(), ".");
}
#[test]
fn tree_of_integers_renders_recursively() {
let t = Tree::node(
Tree::node(Tree::leaf(), 1, Tree::leaf()),
2,
Tree::node(Tree::leaf(), 3, Tree::leaf()),
);
assert_eq!(t.to_string(), "((. 1 .) 2 (. 3 .))");
}
#[test]
fn tree_is_generic_over_any_display_payload() {
let t = Tree::node(Tree::leaf(), "hi", Tree::leaf());
assert_eq!(t.to_string(), "(. hi .)");
}
#[test]
fn tree_display_composes_with_nested_display_types() {
let t = Tree::node(
Tree::node(Tree::leaf(), Color::Red, Tree::leaf()),
Color::Green,
Tree::leaf(),
);
assert_eq!(t.to_string(), "((. Red .) Green .)");
}
}#[cfg(test)]
mod tests {
use super::*;
// --- Approach 1 ---------------------------------------------------------
#[test]
fn color_display_matches_variant_name() {
assert_eq!(Color::Red.to_string(), "Red");
assert_eq!(Color::Green.to_string(), "Green");
assert_eq!(Color::Blue.to_string(), "Blue");
}
#[test]
fn color_display_and_format_macro_agree() {
assert_eq!(format!("{}", Color::Red), "Red");
}
#[test]
fn point_display_uses_one_decimal() {
let p = Point::new(3.0, 4.0);
assert_eq!(p.to_string(), "(3.0, 4.0)");
}
#[test]
fn point_display_rounds_to_one_decimal() {
let p = Point::new(1.23, -2.78);
assert_eq!(p.to_string(), "(1.2, -2.8)");
}
// --- Approach 2 ---------------------------------------------------------
#[test]
fn person_display_is_presentational() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(p.to_string(), "Alice (age 30, alice@ex.com)");
}
#[test]
fn person_debug_quotes_string_fields() {
let p = Person::new("Alice", 30, "alice@ex.com");
assert_eq!(
format!("{p:?}"),
"{ name = \"Alice\"; age = 30; email = \"alice@ex.com\" }"
);
}
// --- Approach 3 ---------------------------------------------------------
#[test]
fn empty_tree_renders_as_dot() {
let t: Tree<i32> = Tree::leaf();
assert_eq!(t.to_string(), ".");
}
#[test]
fn tree_of_integers_renders_recursively() {
let t = Tree::node(
Tree::node(Tree::leaf(), 1, Tree::leaf()),
2,
Tree::node(Tree::leaf(), 3, Tree::leaf()),
);
assert_eq!(t.to_string(), "((. 1 .) 2 (. 3 .))");
}
#[test]
fn tree_is_generic_over_any_display_payload() {
let t = Tree::node(Tree::leaf(), "hi", Tree::leaf());
assert_eq!(t.to_string(), "(. hi .)");
}
#[test]
fn tree_display_composes_with_nested_display_types() {
let t = Tree::node(
Tree::node(Tree::leaf(), Color::Red, Tree::leaf()),
Color::Green,
Tree::leaf(),
);
assert_eq!(t.to_string(), "((. Red .) Green .)");
}
}
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.