A formatting specifier to forward the outer ones

I'm often bothered by:

fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    f.write_str("Hello, ")?;
    self.value.fmt(f)?;
    f.write_str("!")?;
    Ok(())
}

not being writeable with our nice write!() sugar.

That is:

fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "Hello, {}!", self.value)?;
    Ok(())
}

is not equivalent to the previous snippet, but rather, to:

fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    f.write_str("Hello, ")?;
    f.write_fmt(format_args!("{}", self.value))?;
    f.write_str("!")?;
    Ok(())
}

The difference?

The keen eye will already be suspecting something, insofar my fn fmt snippet is on purpose not specifying whether that snippet was occurring in a Display or Debug impl context, whereas our write!() impl already had to "hard-code"/specify one ({} or {:?}).

self.value.fmt(f) // Display or Debug
// vs.
f.write_fmt(format_args!("{}", self.value)) // or we could be using `{:?}`

More generally, by spelling out {} (or {:?}), we are "hard-coding"/specifying not only the formatting trait to be using, but also the rest of the formatting machinery "metadata"/specifiers, such as:

  • expanded: {:}/{:?} vs. {:#}/{:#?}
  • precision: {:}/{:?} vs. {:.4}/{:.4?}
  • padding, etc.

Hence the need, sometimes, to actually go and write the ugly:

f.write_str("Hello, ")?;
self.value.fmt(f)?;
f.write_str("!")?;
  • Playground Demo

    Click to see
    struct A(i32);
    impl ::core::fmt::Display for A {
        fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
            f.write_str("Hi ")?;
            self.0.fmt(f)?;
            f.write_str("!")?;
            Ok(())
        }
    }
    
    struct B(i32);
    impl ::core::fmt::Display for B {
        fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
            write!(f, "Hi {}!", self.0)?;
            Ok(())
        }
    }
    
    fn main() {
        let a = A(42);
        let b = B(42);
        assert_eq!(
            format!("{a:04} {b:04}"),
            "Hi 0042! Hi 42!",
        );
    }
    

My point

Is not to necessarily come up with the One True Solution™, but rather, to kickstart a discussion about potential venues to solve this.

My humble suggestion

would be to add some new sigil formatting specifier; off the top of my head, something like {:!} or {:_} or {:...}.

EDIT: looking at that body, it feels like lack of Placeholder specifiers is already technically possible from the PoV of these callees, which do handle the stuff exactly as I had envisioned it. However, I do not know how to produce such a thing other than manually constructing a fmt::Arguments<'_> instance, which is gated as unstable and internal


A potential follow-up: deep vs. shallow Error : Display specifiers

Once we had this solved, we could finally put an end to the Error : Display convention-deciding debate (as to whether .source()s ought to be displayed as well; or left for the caller to manually walk up the .source() chain), by deciding that:

  • {:#} would display the source error info as well;
  • {} would not.
  • (Or the other way around.)

That way error-chain-printing frameworks could still control this rather nicely (by using the shallow specifier), whilst still allowing a simple/naïve print to give as much info as possible (by using the deep specifier, probably best reserved to the naïve {}), without requiring that everybody implement an error-chain-walker.

6 Likes

I agree that it would be good to have a syntax for "use the formatter's current state to format this".

In your A example, the format spec asked for a width of 4 and the output had a width of 8. I've been in a situation similar to your example, but where I strive to give an output of width 8 when the spec asks for a width of 8.

Using the current state isn't a solution, you need to be able to create a new state.

The unstable FormattingOptions creates the possibility of creating a new Formatter with tweaked options:

In fact, without that features, it is not tenable to fully reconstruct the options. The format specification must be known at compile time, and the fill value has too many possible values. Even ignoring fill, it's annoying to handle every combo of alignment, sign, alternate, etc -- i.e. format options that can't be passed as a separate argument (like width can be).

With that context out of the way: It would be nice if you could specify a base FormattingOptions to use in the spec.

// Your use case, perhaps there is sugar for it
write!(f, "Hi {:!opts$}!", self.0, opts=f.options())
4 Likes

Another issue with formatting's current state is that there's no native way to have alignment/padding for nontrivial values. I created the powerfmt crate to handle this, but it requires manually computing the formatted width, which is far from ideal.

I personally find it intensely dissatisfying to specify the exact formatting for every single parameter that I'm outputting in a Debug impl. Oftentimes, I'll want the debug formatting to be single-line in all cases (say, a VirtualAddress(0x123456)), but also use pretty hexadecimal for the integer parameters. This is where I have to whip out the ugly syntax, rather than using something nice and expressive like write!(), as suggested here.

+1.

3 Likes