Making format machinery more amenable to dynamic formatting

For a project I have to implement some formatting of values with a user-provided format string. Unfortunately, all crates that allow this are either nightly-only (rt_format) or incomplete.

I'm fine with parsing the format string myself (although Rust could expose its own routine to do it in the library), but there is no stable way to pass formatter parameters, like align/width/precision, dynamically. And I really don't want to reimplement number formatting for all the different types.

I see two ways out of this:

  • Large impact: make the whole V1 machinery public and stable. Its naming is versioned after all, so I don't see the reason to keep it unstable; a new version would probably introduce new V2 APIs? (Also, has the V1 API changed incompatibly at all since Rust 1?)

  • Less impact: make the formatting settings of the Formatter read-write accessible. For example, fill() could get a setter set_fill() and so on. I'm not aware of a downsides here, and the new API surface is pretty minimal and does not expose any of the V1 machinery. It does mean that Debug/Display/... impls can change user intention given by the format string, but they're already free to ignore it completely and just write whatever to the formatter.

  • Alternative, also less impact: allow stable creation of an Arguments instance with flags selected at runtime, for the very restricted case of only one value to format. This can then be passed to write_fmt, again without exposing any V1 details.

What do you think? Does this need an RFC? I'm obviously willing to work on any chosen solution.

Another alternative (which is probably unattractive for you and your immediate problem, but nonetheless): spend some designing a comprehensive V2 to replace V1, which is amenable for making public & stabilization.

FWIW, I've wanted to use the formatting internals a bunch of times, so it would be nice to (finally) make some steps towards that. Personally I don't have any short-term concerns, so figuring out the right APIs for the long-term would have my preference (and not cluttering the API with a V1 seems preferable in the absence of backwards compatibility concerns).

1 Like

There is: you can pass both width and precision as named or positional parameters with the $ syntax.

You can't pass alignment or sign dynamically, but they have few enough choices that you could map all the combinations to different format calls, or in the case of alignment, format as a string first then format that string with an alignment.

(That said, we ought to have a syntax to dynamically pass alignment and sign parameters.)

I struggle with that syntax every single time, FWIW.^^ Too many sigils. Also, postfix-$ is really weird syntax.

Here's how it looks (took me like 5min to get valid syntax):

fn main() {
    let width = 22;
    let n = 1024;
    println!("{:>width$}", n, width=width);
}

EDIT: Ah, there's an example in the docs that I missed so far because it is below the grammar... (There were examples above so when the grammar came I assumed that means examples are done.)

I opened a PR to reorder the docs to make them more readable.

1 Like

I know, and that's going to be my workaround for now (even if something is done here, it'll take a while until stabilization). But I'm limiting myself to two settings (sign and zero-padding) since combinatorial explosion is at hand...

And alternate flag, and zero-padding, and fill character. The last is especially tricky since it can be any character, even }! (TIL.)

Don't get me wrong, I'd be happy if it was possible in that way, but it's going to be a nightmare to find a syntax that is both compatible and at least not less obvious than the existing $ and * sigils.

But it would be much easier (to define and use) to construct a struct equivalent of the format string in normal Rust, which is what an Arguments method as in my third suggestion would do.

There was a thread a while ago awhile ago about speeding up formatting with a new behind-the-scenes implementation:

1 Like

Though maybe not so ergonomic, when I have some complex formatting I end up writing something like

fn display_options(val: &Val, options: Options) -> impl Display + '_ {
    // ...
}
println!("{}", display_options(&val, options));