You’re implementing fmt::Display
wrong
Posted by Michał ‘mina86’ Nazarewicz on 12th of May 2024 | (cite)
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); }