You’re implementing fmt::Display wrong

Posted by Michał ‘mina86’ Nazarewicz on 12th of May 2024

TL;DR: When implementing Display trait for a wrapper type, use self.0.fmt(fmtr) rather than invoking write! macro. See The proper way section below.

Imagine a TimeOfDay type which represents time as shown on a 24-hour clock. It could look something like the following:

pub struct TimeOfDay {
    pub hour: u8,
    pub minute: u8,
}

impl core::fmt::Display for TimeOfDay {
    fn fmt(&self, fmtr: &mut core::fmt::Formatter) -> core::fmt::Result {
        write!(fmtr, "{:02}:{:02}", self.hour, self.minute)
    }
}

fn main() {
    let hour = 2;
    let minute = 5;
    assert_eq!("02:05", TimeOfDay { hour, minute }.to_string());
}

White it’s a serviceable solution, one might tremble at the lack of type safety. Nothing prevents the creation of nonsensical times such as ‘42:69’. In real life hour rarely goes past 23 and minute sticks to values below 60. Possible approach to prevent invalid time is to use a newtype idiom with structs imposing limits on the wrapped value, for example:

use core::fmt;

struct TimeOfDay {
    hour: Hour,
    minute: Minute,
}

struct Hour(u8);
struct Minute(u8);

impl Hour {
    fn new(val: u8) -> Option<Self> { (val < 24).then_some(Self(val)) }
}

impl Minute {
    fn new(val: u8) -> Option<Self> { (val < 60).then_some(Self(val)) }
}

impl fmt::Display for TimeOfDay {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        write!(fmtr, "{:02}:{:02}", self.hour, self.minute)
    }
}

fn main() {
    let hour = Hour::new(2).unwrap();
    let minute = Minute::new(5).unwrap();
    assert_eq!("02:05", TimeOfDay { hour, minute }.to_string());
}

Alas, since the new types don’t implement Display trait, the code won’t compile. Fortunately the trait isn’t complicated and one might quickly whip out the following definitions:

impl fmt::Display for Hour {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        write!(fmtr, "{}", self.0)
    }
}

impl fmt::Display for Minute {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        write!(fmtr, "{}", self.0)
    }
}

Having Display, Debug, Octal etc. implementations which call write! macro only is quite common. But while common, it’s at times incorrect. While the above example with such definitions will build, the test in the main will fail (playground) producing the following error:

thread 'main' panicked at src/main.rs:40:5:
assertion `left == right` failed
  left: "02:05"
 right: "2:5"

The issue is that invoking write! erases any formatting flags passed through the fmtr argument. Even though TimeOfDay::fmt used {:02} format, the Display implementations disregard the width and padding options by calling write! with {} format.

Fortunately, the solution is trivial and in fact even simpler than calling write!.

The proper way

In majority of cases, the proper way to implement traits such as Display or Debug is to use delegation as follows:

impl fmt::Display for Hour {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        self.0.fmt(fmtr)
    }
}

impl fmt::Display for Minute {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        self.0.fmt(fmtr)
    }
}

Since the same Formatter is used, any configuration that the caller specified (such as width and fill) will be applied when formatting the inner type (playground).

In fact, there is a crate for that. derive_more offers derives for various traits including Display. When used with no additional options on a newtype struct, the crate will generate a delegating implementation of the trait. In other words, the above impls can be replaced by the following derive annotations:

#[derive(derive_more::Display)]
struct Hour(u8);

#[derive(derive_more::Display)]
struct Minute(u8);

Display vs Debug

Related trick is delegating between Display and Debug traits (or any other formatting traits). This is especially useful when implementation for both types should be identical. A naïve approach would be to use something like write!(fmtr, "{self:?}") in Display implementation but this suffers from aforementioned issues. Delegation is once again the better approach (playground):

use core::fmt;

#[derive(Debug)]
enum DayOfWeek {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

impl fmt::Display for DayOfWeek {
    fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
        fmt::Debug::fmt(self, fmtr)
    }
}

fn main() {
    println!("dbg={:?} disp={}", DayOfWeek::Monday, DayOfWeek::Monday);
}