Define fmt::Display and fmt::Debug together
Asked Answered
E

2

7

Is it possible to define fmt::Display and fmt::Debug together, i.e. such that they use the same implementation?

Assume we have the below sample code (purely for demonstration purposes):

use std::fmt;

struct MyDate {
    pub year: u16,
    pub month: u8,
    pub day: u8
}

impl PartialEq for MyDate {
    fn eq(&self, other: &Self) -> bool {
        self.year == other.year && self.month == other.month && self.day == other.day
    }
}

impl fmt::Display for MyDate {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}-{:02}-{:02}", self.year, self.month, self.day)
    }
}

impl fmt::Debug for MyDate {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self)
    }
}

fn main() {
    let d1 = MyDate { year: 2008, month: 9, day: 10 };
    let d2 = MyDate { year: 2008, month: 9, day: 10 };

    println!("Date = {}", d1);  // requires fmt::Display
    assert_eq!(d1, d2);         // requires fmt::Debug
}

It seems that in order to print my class using println and write unit tests using assert_eq, I need to define both fmt traits. Is there a way to have 1 "print" implementation that simultaneously defines fmt::Display and fmt::Debug? For example something along the lines of impl fmt::Debug, fmt::Display for MyDate { ... } or similar?

Or is it common practice to just put #[derive(Debug)] in front of any class? Apologies if my question is naïve, as I am new to rust.

Ecumenicist answered 14/11, 2021 at 15:12 Comment(0)
R
5

In most cases, Display and Debug have different output. When it makes sense to use the same formatting in both cases, the code you've written is fine. It would also be fine to #[derive(Debug)] — it's your decision to make. I think the important thing to keep in mind is that Debug should not leave anything out, and be unambiguous.

In the case you show, the Display format shows all of the struct's fields completely accurately, so it's perfectly fine to reuse Display to implement Debug. If you had a lot of types like this, you could use a simple macro to generate Debug implementations that forward to Display.

By the way, I would suggest using #[derive(PartialEq)] instead of the manual PartialEq implementation you show. It's completely equivalent, and avoids the dangerous failure mode of forgetting to compare a newly added field.

Redfaced answered 14/11, 2021 at 17:8 Comment(2)
Thanks, all of this makes sense. Just to double-check, there is no way to implement function fmt for both traits at the same time, like impl fmt::Debug, fmt::Display for MyDate { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ... } } or similar?Ecumenicist
@Ecumenicist A macro could do that, but the built-in syntax of Rust requires one complete impl block per trait you're implementing.Redfaced
G
0

Performance consideration: in your Debug implementation, you are calling the Display implementation of the same object:

impl fmt::Debug for MyDate {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self) // call method from Display
    }
}

However, when doing this, Debug::fmt() calls write! twice – and this is not a cheap operation.

A better implementation would be calling directly the method from the Display trait:

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

Now write! is called only once.

Or is it common practice to just put #[derive(Debug)] in front of any class?

Yes, it's very common. Implementing your custom Debug is interesting only if you want a custom output of the fields in your struct.

Geller answered 28/8, 2023 at 1:47 Comment(2)
Please elaborate on "not a cheap operation". Yes using fmt::Display::fmt(self, f) is 1 write! instead of 2, when looking directly at the code. However, the compiler is smart and actually replaces write!(f, "{}", self) with fmt::Display::fmt(self, f). Regardless, I wouldn't call write! in itself expensive. Overall Rust's formatting framework is cheap. Additionally (overall) no allocations are performed while formatting. So I don't see what would make it VERY expensive?Stephie
I suppose "expensive" is from a compile-time perspective, since each macro call increases compile-time...?Performance

© 2022 - 2024 — McMap. All rights reserved.