rust

Is there a nicer way to implement Display for structs that own collections of things with Display?


I keep finding myself writing Display for structs that hold Vec of some type that implements Display. For example:

use std::fmt::Display;
struct VarTerm {
    pub coeffecient: usize,
    pub var_name: String,
}
impl Display for VarTerm {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}{}", self.coeffecient, self.var_name)
    }
}
struct Function {
    pub terms: Vec<VarTerm>,
}
impl Display for Function {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let strings = self
            .terms
            .iter()
            .map(|s| format!("{}", s))
            .collect::<Vec<String>>()
            .join(" + ");
        write!(f, "{}", strings)
    }
}
fn main() {
    let my_function = Function {
        terms: vec![
            VarTerm {coeffecient: 2,var_name: "x".to_string(),},
            VarTerm {coeffecient: 4,var_name: "y".to_string(),},
            VarTerm {coeffecient: 5,var_name: "z".to_string(),},
        ],
    };
    println!("All that work to print something: {}", my_function)
}

This looks bulky and ugly to me in a bunch of places - coming from higher-level languages I'm never a fan of the .iter()/.collect() sandwiching (I kind of get why it's needed but it's annoying when 90+ percent of the time I'm just going from Vec to Vec). In this case it's also compounded by the format!() call, which I swear has to be the wrong way to do that.

I'm not sure how much of this is inherent to Rust and how much is me not knowing the right way. I want to get as close as possible to something like:

self.terms.map(toString).join(" + "), which is about what I'd expect in something like Scala.

How close can I get to there? Along the way, is there anything to be done about the aforementioned iter/collect sandwiching in general?


Solution

  • In an eerie coincidence, literally 2 minutes ago I looked at a few methods in the itertools crate. How about this one:

    https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.join

    fn join(&mut self, sep: &str) -> String
    where
        Self::Item: Display
    

    Combine all iterator elements into one String, separated by sep.

    Use the Display implementation of each element.

    use itertools::Itertools;
    
    assert_eq!(["a", "b", "c"].iter().join(", "), "a, b, c");
    assert_eq!([1, 2, 3].iter().join(", "), "1, 2, 3");
    

    EDIT: Additionally, whenever you ask yourself if there was a nicer way to implement a particular trait, especially when the implementation would be somewhat recursive, you should look if there's a derive macro for that trait. Turns out there is, albeit in a separate crate:

    https://jeltef.github.io/derive_more/derive_more/display.html

    Example:

    #[derive(Display)]
    #[display(fmt = "({}, {})", x, y)]
    struct Point2D {
        x: i32,
        y: i32,
    }