Skip to content

← Generics and Traits step 6 of 24

Easy Primitives

Display, Debug, and the ToString blanket impl

Rust has two formatting traits and they are for two different audiences.

  • Debug{:?} — is for programmers. Derive it (#[derive(Debug)]) on almost everything. Output is allowed to be ugly and to change.
  • Display{} — is for users. You must write it by hand, because only you know what “showing this to a human” means.

This exercise is about Display, and about a piece of std machinery that catches people out the first time.

The blanket impl

The standard library contains, roughly:

impl<T: Display + ?Sized> ToString for T {
    fn to_string(&self) -> String { /* format into a String */ }
}

That is a blanket impl: one impl covering every type at once, conditional on a bound. The consequence is immediate and load-bearing — implementing Display gives you .to_string() for free. You must never implement ToString yourself, and you must never add an inherent fn to_string(&self) to your type either.

Three separate default-on lints police exactly this boundary:

  • clippy::to_string_trait_impl — you wrote impl ToString for Money
  • clippy::inherent_to_string — you wrote impl Money { fn to_string(&self) }
  • clippy::inherent_to_string_shadow_displaycorrectness, deny — you did that and also implemented Display, so which one runs now depends on method resolution rather than on what you meant

The starter walks straight into the first one. Read the error, then move the logic where std wants it.

A correctness lint that will save you

clippy::recursive_format_impl is deny-by-default and catches this:

impl Display for Money {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self)     // infinite recursion, stack overflow
    }
}

Beginners write that constantly — self is right there and {} is the thing you are implementing. Format the fields, never self.

Your task

pub struct Money { pub cents: i64 }
impl Display for Money { ... }
pub fn render(cents: Vec<i64>) -> Vec<String>

Render an amount in cents as currency, thousands-grouped:

cents output
123456 $1,234.56` | | `0` | `$0.00
5 $0.05` | | `-123456` | `-$1,234.56
100000000 $1,000,000.00` | | `i64::MIN` | `-$92,233,720,368,547,758.08

The sign goes outside the dollar mark. The fractional part is always two digits — {:02} in a format string. render must produce its strings by calling .to_string(), which is your proof the blanket impl fired.

Watch the negative-numbers edge. -self.cents overflows for i64::MIN, and under -O there are no overflow checks, so it would silently produce i64::MIN again and print the wrong sign. i64::unsigned_abs returns a u64 and is the correct tool.

One formatting fact

Formatter makes no newline guarantee and adds nothing of its own. What you write is exactly what comes out, which is why the expected strings here contain no whitespace you did not put there. write!(f, ...) returns fmt::Result; propagate it with ? and return the last one.

Remember the grade is compile + tests + clippy -D warnings.