ExamplesBy LevelBy TopicLearning Paths
083 Intermediate

083 — Display Trait

Functional Programming

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

  • • Implement fmt::Display using write!(f, "...", ...) in the fmt method
  • • Understand fmt::Formatter as the sink that write! targets
  • • Use format specifiers like {:.1} for floating-point precision inside Display
  • • Implement Display for generic types with T: fmt::Display bound
  • • Distinguish Display (user-facing) from Debug (developer-facing)
  • • Map Rust's trait-based formatting to OCaml's explicit to_string functions
  • Code 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

    AspectRustOCaml
    Interfaceimpl fmt::Display traitto_string function per type
    Generic elementsT: fmt::Display boundto_s : 'a -> string parameter
    Debug format#[derive(Debug)]Printf.sprintf "%S" / ppx
    to_string()Auto from DisplayExplicit function
    Sink typefmt::FormatterReturns 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 .)");
        }
    }
    ✓ Tests Rust test suite
    #[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

  • • Write to_string function manually
  • • Use Printf.sprintf with format strings
  • • No unified "display" protocol
  • Rust Approach

  • impl fmt::Display for Type
  • • Enables format!("{}", x), println!("{}", x)
  • • Single fmt method returns fmt::Result
  • Comparison Table

    FeatureOCamlRust
    ProtocolManual to_stringimpl Display
    Format string%s with to_string{} automatic
    Debug#show (ppx)#[derive(Debug)]
    DeriveNoDisplay: no, Debug: yes

    Exercises

  • Implement fmt::Display for a Matrix(Vec<Vec<f64>>) newtype that prints rows separated by newlines.
  • Add an impl fmt::Display for Tree<T> variant that prints in indented format (each level adds two spaces).
  • Implement fmt::Debug manually for Person to show Person { name: "Alice", age: 30, email: "…" }.
  • Write a Wrapper<T: Display>(Vec<T>) that displays its items as [a, b, c].
  • In OCaml, define a Printable module type with val to_string : t -> string and a functor PrintList(P: Printable) with val print_list : P.t list -> string.
  • Open Source Repos