Should `Display` implementations respect width, fill, align?

Given that Display is given formatting parameters in fmt::Formatter, it would seem that implementations should normally respect these.

However, most people ignore these, and even the example given in Display docs ignores them!

If everybody except a few standard types is ignoring these, is the trait badly designed?

6 Likes

Personally I'm different, given that basically no one supports it. With that said, it was requested that I support it for time, so I threw together a new library called powerfmt to make doing so much easier.

Essentially, you have to implement a different trait and provide the formatted width, at a minimum. As doing so isn't necessarily free, you have the ability to store metadata that is later used to format the type without recalculation. If you don't want that metadata to be public, just put an attribute on its declaration. A Display implementation can be delegated to the implementation of SmartDisplay with a different attribute.

If you're interested in the crate, check out the docs — particularly the smart_display module. I have been using this with time for a number of months without issue, so it definitely works.

4 Likes

There’s certainly a question about how compound types should interpret these parameters: Should they control the overall representation, or should they be passed on to control the display of the component types?

In other words, should the “width” of a list control:

  • The number of character columns emitted,
  • The number of unicode scalar values emitted,
  • The number of bytes emitted,
  • The number of list elements emitted (presumably with some kind of elision indicator),
  • The width of each individual list element including separators, or
  • The width of each individual list element exclusive of separators?

Similar questions can be asked of the precision and alignment parameters.


If this gets changed, I’d like to see the set of format parameters be extensible somehow. But that does create a risk for generic usage: If types don’t agree on what various parameters mean, there’s a risk that generic code won’t be able to produce the layout they need to.

If nothing else, it would be nice to pass through some multi-line layout information like starting column, max line length, etc.

I ran into this when implementing display for a severity level enum in a command line program. I was adding colour codes (using anstyle/anstream). Had to specifically not count the width of the control codes. Overall it is a bit of a pain to do this properly currently.

The way I see it is that the Display trait shouldn't have any configuration. Any custom configuration you want can always be implemented by wrapper types:

println!("{}", pi.scientific().with_precision(5).in_base(17));

And format! could potentially translate to something like that.

4 Likes

Ironically, I try to support formatting in my Display implementations but have only used formatting when printing numbers and then rarely.

Field width is nice if you are making a simple table on stdout. Something simple when you don't need a full blown TUI library.

3 Likes

I like the general approach, but we might want to think about some ancillary traits to help with layout for compound types. For example, how would you write a Display implementation for a 2-dimensional Matrix<T> type (or an appropriate view wrapper) that wants to make its columns line up correctly?

My first thought would be an API like this:

trait FixedWidthDisplay: Display {
    const WIDTH:usize;
}

impl<T> Matrix<T> {
    fn format_elems<E:FixedWidthDisplay>(&self, f:impl Fn(&T)->E)->impl FixedWidthDisplay {
        unimplemented!()
    }
}

Along similar lines, it would be nice for Formatter to have methods that can query the current column number to help with vertical alignment of multiline values.

Maybe I just missed the (easy) way to do it, but I once wanted to print numbers (f64) aligned in tabular form to the terminal and somehow could not figure out how to do it with formatting parameters, so I ended up writing my own half-backed code that relied on String operations (padding and truncation).

(side remark: in most languages, when I print floating point numbers, it automatically reasonably picks between simple format (e.g. 123.456) and scientific notation (e.g. 1.23456e100), while Rust seems to have no hesitation printing 1234560000000000000000000000000000000000000000 or 0.000000000000000000000000000000000000000000000123456, which is really annoying. Maybe there's a better way, but I haven't found it, yet)

This specific thing is done by ryu.

Formatting

This library tends to produce more human-readable output than the standard library’s to_string, which never uses scientific notation. Here are two examples:

  • ryu: 1.23e40, std: 12300000000000000000000000000000000000000
  • ryu: 1.23e-40, std: 0.000000000000000000000000000000000000000123

Both libraries print short decimals such as 0.0000123 without scientific notation.

As for alignment, it's probably easiest to just keep an allocated String around and write to that, then align the String.

Oh, wow. Thank you for pointing out the crate that can do this.

I was desperately hoping someone would say "Of course this can be done in std, here's the flag."

std should do this by default.

I don't think anyone would want to see floats printed as 123456000000000000000000000000000000000000000000000000000000000000000 or 0.00000000000000000000000000000000000000000000000000000000000000000000000000123456.

2 Likes

I wrote up what follows and then looked to see if anything had changed. And it has! This ACP-accepted proposal would fix a lot of things. (Last I looked, making fill dynamic was the only thing I found, IIRC.)


Sometimes I support the format spec inputs the best I can, but it's a pain. I don't think the system is the most well designed or intuitive thing. Here's how you read the format specification as an implementor...

Format Spec Input fmt::Formatter<'_> method Notes
Fill fill -> char Static (!)
Alignment align -> Option<Alignment> Static
Sign sign_minus -> bool, sign_plus -> bool Static
Alternative alternate -> bool Static
Zero sign_aware_zero_pad -> bool Static
Width width -> Option<usize> Dynamic
Precision precision -> Option<usize> Dynamic
Debug Hex Case debug_lower_hex -> bool, debug_upper_hex -> bool Private methods!

First of all, the only way to adjust these is to create a new Formatter<'_> by using some format-str taking macro. If you just pass the &mut fmt::Formatter<'_> on to your sub-fields,[1] you're passing along all the spec inputs as well.

Second of all, by "Static" in the last column, I mean the value of the spec input has to be in your (literal) format-str; it can't be a dynamic parameter. So if you want to, say, adjust the width and pass on other things like Alternate, you have to do something like...[2]

// (Ignoring `align` and `sign_aware_zero_pad` for this example)
match (f.sign_minus(), f.sign_plus(), f.alternate()) {
    (false, false, false) => write!(f, "{:width$}", field, width=...)?,
    (false, false, true ) => write!(f, "{:#width$}", field, width=...)?,
    // ...

And you simply cannot reasonably preserve the fill using the formatting system[3] (for more than a handful of expected and hard-coded values, anyway).

It'd be nicer if you could...

impl<'a> Formatter<'a> {
    fn set_fill(&mut self, fill: char) { /* ... */ }
    fn set_alignment(&mut self, alignment: Option<Alignment>) { /* ... */ }
    // ...

    // And perhaps even
    /// Reborrow this `Formatter`.  The reborrowed `Formatter` has
    /// it's own copy of the input parameters, which can be modified
    /// independently of the original `Formatter`.
    fn borrow(&mut self) -> Formatter<'_> { /* ... */ }

(Third of all, there should be a sign -> Option<Sign> or such, not a method for each of - and +.[4] Although flags is deprecated, it still shines through.)

((Fourth of all, you have to jump through hoops to use the struct helpers in a Display implementation,[5] since they're bound on T: Debug.))


  1. self.field.fmt(f) ↩︎

  2. 48 cases ignoring fill (and "type") ↩︎

  3. versus writing to a String and manipulating that or such ↩︎

  4. which cannot both be specified in a format-str ↩︎

  5. or Binary etc ↩︎

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.