Skip to content

[ACP] Provide an interface for creating instances of fmt::Formatter #286

Closed
@EliasHolzmann

Description

@EliasHolzmann

Edit 2023-10-23: Added a bit of discussion about why a new function on Formatter instead of the builder pattern is problematic

Follow-up to #280.

Proposal

Problem statement

Creating a custom Formatter at runtime as well as modifying an existing Formatter is currently not supported, one can only use the Formatters passed in by the formatting macros. Being able to create and modify them could be useful for multiple use cases:

  1. Calling the formatting code of contained fields with a modified formatting parameter
  2. Exposing different formatting methods for different representations/data points on one struct
  3. Exposing formatting methods that require additional data
  4. Writing libraries that provide enhanced formatting capabilities

Motivating examples or use cases

  1. Allow modifying Formatter members rust#19207
  2. Feature request: Add a way to capture a formatter that can be passed to format helper functions rust#74870
  3. Unable to create Formatter rust#46591
  4. Multiple instances fit this use case:
    • runtime-fmt (a formatting crate that allows the user to supply the format string at runtime) seems to use the unstable fmt_internals feature to build a custom std::fmt::Arguments which has the Formatter baked in (or rather the mostly equivalent fmt::rt::Placeholder struct), see here. In consequence, this crate requires nightly Rust. (Note that the interface isn't the current one as runtime-fmt hasn't been updated since 2019)
    • rt_format (another runtime formatting crate) handles the problem in another way: Using a macro named generate_code!, it generates a function containing a format_args! call for every combination of alignment, signedness, default/alternative representation, zero/space padding, width (via variable), precision (via variable) and formatting trait for a total of 1024 format_args! invocations at the time of writing. Fill characters are not supported, as those cannot be passed via a variable to the format_args! call but must be part of the format specifier. (If you are interested to see the result of this approach, run cargo expand in the crate root and search for a huge function named format_value)
    • I'd like to experiment with a crate that reimplements the formatting macros but adds some additional features (mostly interpolation). However, it is currently impossible to do this in a manner that is compatible with the std implementation outside of nightly Rust (rt_format is almost there, but they cannot support fill characters, apart from their solution being quite hacky and probably inefficient).

Solution sketch

In std::fmt:

struct FormatterBuilder<'a>;
impl<'a> FormatterBuilder<'a> {
    /// Construct a new `FormatterBuilder` with the supplied `Write` trait object for output that is equivalent to the `{}` formatting specifier (no flags, filled with spaces, no alignment, no width, no precision).
    fn new(write: &'a mut (dyn Write + 'a)) -> Self;
    /// Constructs a new `FormatterBuilder` that is equivalent to a given `Formatter`.
    fn new_from_formatter(fmt: &'a mut Formatter<'a>) -> Self
    /// Copies all formatting properties from `other`, only the `Write` trait object is kept
    fn with_formatting_from(mut self, other: &Formatter) -> Self;
    
    fn sign_plus(mut self, sign_plus: bool) -> Self;
    fn sign_minus(mut self, sign_minus: bool) -> Self;
    fn alternate(mut self, alternate: bool) -> Self;
    fn sign_aware_zero_pad(mut self, sign_aware_zero_pad: bool) -> Self;
    fn fill(mut self, fill: char) -> Self;
    fn align(mut self, align: Option<Alignment>) -> Self;
    fn width(mut self, width: Option<usize>) -> Self;
    fn precision(mut self, precision: Option<usize>) -> Self;
    
    /// Builds the `Formatter`.
    fn build(self) -> Formatter<'a>;
}

Note that all FormatterBuilder methods take and return self by value. This is a bit unergonomic, but necessary: build must take self by value in order to transfer ownership of the Write trait object to the returned Formatter.

Alternatives

  • This proposal passes the output stream as a dyn Trait to the new function. If in the future, Formatter gets refactored to be generic struct with the output struct used as a generic type parameter, fn new will not match this new interface. However,
    1. I don't see how the proposal could be implemented instead, and
    2. I don't believe refactoring Formatter in this fashion is possible anyway – this would change the interface and therefore would be a breaking change.
  • Allowing to modify Formatters instead of creating new ones is problematic, see [ACP] Provide an interface for creating and modifying instances of fmt::Formatter  #280 (comment)
  • Having a new function on Formatter instead of the builder pattern is problematic if formatting specifiers get additional parameters added someday (to my knowledge, there aren't any plans, but I believe it is at least within the realms of possibility). Also, this method would take 9 parameters – it wouldn't be very clear to the reader of the code what is happening there
  • At a first glance, new_from_formatter seems to be superfluous as it is equivalent to FormatterBuilder::new(fmt).with_formatting_from(fmt). However, there are two reasons this function is necessary (or at least very nice to have):
    1. new_from_formatter can set the underlying Write trait object to the Write trait object contained in fmt instead of the Write impl of fmt itself, thereby avoiding one lookup in the virtual method table (though rustc may be able to optimize this out anyway)
    2. More importantly: new takes write as a mutable reference, so the borrow checker wouldn't allow calling with_formatting_from on the mutably borrowed fmt.

Links and related work

Relevant previous libs team discussion: rust-lang/rfcs#3394 (comment)
Previous ACP with a different approach: #280

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions