Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

rust

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?

like image 750
Edward Peters Avatar asked Sep 06 '25 03:09

Edward Peters


2 Answers

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,
}
like image 138
Lagerbaer Avatar answered Sep 07 '25 21:09

Lagerbaer


If you find yourself repeating this a lot you might want to move the .into_iter()/.collect() into a specific trait at the cost of generality and composability. You can also pass ToString::to_string to map.

impl Display for Function {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let strings = self.terms.map(ToString::to_string).join(" + ");
        f.write_str(&strings)
    }
}
trait VecMap<'a, T: 'a, U>: IntoIterator<Item = T> {
    fn map(self, f: impl FnMut(T) -> U) -> Vec<U>;
}
impl<'a, T: 'a, U> VecMap<'a, &'a T, U> for &'a Vec<T> {
    fn map(self, f: impl FnMut(&'a T) -> U) -> Vec<U> {
        self.into_iter().map(f).collect()
    }
}
like image 36
cafce25 Avatar answered Sep 07 '25 21:09

cafce25