Rust: Formatting Wrapper Types
Today, I came across this blog post You’re implementing fmt::Display wrong where the author discusses the caveats of implementing the Display trait for wrapper types. I didn’t fully understand the explanation at first, but going back and forth with DeepSeek R1 helped me grasp it better.
use core::fmt;
struct FancyHour {
hour: Hour,
}
struct Hour(u8);
impl fmt::Display for FancyHour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// This {:02} is not used because self.hour uses its own {} formatting
// operation instead.
write!(fmtr, "{:02}", self.hour)
}
}
impl fmt::Display for Hour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// Apparently, because we are using write!(), any "{}" will start a new
// formatting operation, which means we don't honor FancyHour's "{:02}"
// write!(fmtr, "{}", self.0)
// Correct way is to forward the printing
self.0.fmt(fmtr)
}
}
fn main() {
let hour = Hour(2);
let hour_of_day = FancyHour { hour };
println!("{}", hour_of_day); // This not printing 02 instead prints 2
}
DeepSeek R1 has given the best explanation:
The issue lies in how formatting traits forward formatting parameters. Let’s break it down:
Key Problem
The {:02} format specifier in FancyHour’s implementation does get
passed to Hour’s Display implementation. However, your Hour’s
Display implementation is ignoring these formatting parameters by using
write!(fmtr, "{}", self.0) instead of forwarding them.
Detailed Explanation
- Formatting Flow:
FancyHour::fmt (with {:02}) → Hour::fmt (with {:02} specifier)
The {:02} specifier is passed to Hour’s fmt method through the Formatter.
- The Mistake in
Hour::fmt:
write!(fmtr, "{}", self.0) // ❌ Creates NEW formatting operation
This starts a fresh formatting operation with {} (default formatting),
discarding the :02 specifier.
- The Fix:
self.0.fmt(fmtr) // ✅ Forwards existing formatting parameters
This forwards the existing Formatter with its :02 specifier to the
u8’s implementation.
Fixed Code
use core::fmt;
struct FancyHour {
hour: Hour,
}
struct Hour(u8);
impl fmt::Display for FancyHour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
write!(fmtr, "{:02}", self.hour) // Passes :02 to Hour::fmt
}
}
impl fmt::Display for Hour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// Forward formatting parameters to the inner u8
self.0.fmt(fmtr) // ✅ Uses the existing Formatter with :02
}
}
fn main() {
let hour = Hour(2);
let hour_of_day = FancyHour { hour };
println!("{}", hour_of_day); // Now prints "02"
}
Why This Works
self.0.fmt(fmtr)preserves the originalFormatterwith its:02specifieru8’s nativeDisplayimplementation understands format specifiers like:02- The
write!macro inHour::fmtwas creating a new formatting operation, discarding the padding request
Key Takeaway
When implementing formatting traits for wrapper types:
- Use
.fmt(fmtr)to forward formatting parameters to inner values - Avoid
write!(fmtr, "{}", ...)for wrapper types unless you explicitly want to reset formatting options
This preserves the formatting context (width, padding, alignment, etc.) requested by the caller.
Vineel: Q: how is write!(fmtr, “{}”, self.0) creating new formatting operation isn’t the first argument fmtr the existing formatter with :02 ?
DeepSeek R1: You’re absolutely correct to question this—it’s a subtle aspect of how Rust’s formatting machinery works. Let’s break it down:
The Core Issue: Formatting Contexts
When you use write!(fmtr, "{}", self.0), you’re starting a new
formatting operation with its own format specifier ({}), even though
you’re reusing the same Formatter (fmtr). This new operation overrides
any previous formatting parameters (like :02) because:
write!parses the format string ("{}"in this case) to determine how to format the arguments.- Each
write!macro call resets the formatting context for its arguments, using the specifiers in its own format string.
Why {:02} Doesn’t Propagate
In your code:
impl fmt::Display for FancyHour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// Intends to pass `:02` to `self.hour`
write!(fmtr, "{:02}", self.hour)
}
}
impl fmt::Display for Hour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// ❌ Starts a NEW formatting operation with `{}`, overriding `:02`
write!(fmtr, "{}", self.0)
}
}
- The
{:02}inFancyHouris passed toHour::fmtthrough theFormatter. - But
Hour::fmtthen starts a new formatting operation with"{}", which resets the specifier to default (ignoring:02).
The Fix: Forward the Existing Formatter
Instead of starting a new formatting operation with write!, directly
delegate formatting to the inner u8’s fmt method:
impl fmt::Display for Hour {
fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
// ✅ Forwards the EXISTING formatter (with `:02`) to `self.0`
self.0.fmt(fmtr)
}
}
This preserves the original formatting parameters (like :02) because it
doesn’t reset the context with a new format string.
Analogy: A Chain of Commands
Imagine the Formatter is a memo being passed between people:
FancyHourwrites: “Format this hour with 2 digits, padded with zeros (:02).”Hourreceives the memo but then writes a new memo: “Format this number with default settings ({}).”u8sees only the second memo and ignores the original:02request.
By using self.0.fmt(fmtr), Hour simply forwards the original memo
(:02) to u8.
Key Takeaway
write!(fmtr, "{}", ...)creates a new formatting operation with its own specifiers..fmt(fmtr)forwards the existing formatter (including its specifiers like:02).
This is why directly calling self.0.fmt(fmtr) works, while write! resets
the context. The distinction is subtle but critical for preserving
formatting parameters across nested types.