Skip to content

Reduce the number of format specifiers #9807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
orenbenkiki opened this issue Oct 11, 2013 · 6 comments
Closed

Reduce the number of format specifiers #9807

orenbenkiki opened this issue Oct 11, 2013 · 6 comments

Comments

@orenbenkiki
Copy link

Given the existence of {} and the Default trait, there's no real value in {:i}, {:u}, {:t}, etc. other than for providing very rudimentary type checking for the parameters passed to format!.

It would be simpler to just use {} everywhere and only use specifiers for things like changing the base of printed integers (that is, only keep specifiers such as {:x} which actually affect the way the printing is done).

If specifiers are only used for changing the base of printed integers, then their implementation can be simplified to just setting a current base member of the format which would be available to the implementation of the formatting in the Default trait, instead of using a trait per each specifier.

This way we'd end up with having to implement just one trait for printing any type, including number-like types, instead of having to implement multiple traits (one per base).

@chris-morgan
Copy link
Member

Here's a use case to consider: in rust-http, I've implemented Default for Status as something that produces results like 418 I'm a teapot (I should perhaps also be implementing String with the same effect) and Unsigned as something that produces 418 (using self.code(), which is a u16). I can't think of any situation where anyone would want to format the status code as binary, hex or octal, but making it well-rounded would indeed suggest that Binary, Octal, LowerHex and UpperHex should be implemented.

Delegated derivation, as has been suggested for a few other things, could be appropriate here for some of these types of use cases.

Of course, not all number types will be able to be done with just a single trait. Some may need to be done with multiple traits, if they have a fundamentally different representation of the number from what is supported at present. (Even big numbers probably need manual implementations?)

@orenbenkiki
Copy link
Author

I was thinking that something simpler could be used. If {:x} was viewed as "invoke {} but during this invocation, the base field of the Formatter will be set to 16", then we wouldn't need traits like LowerHex. Instead, we'd only have Default (possibly renamed), which will be able to query the base field and do the right thing.

This way, if you implement your Status formatting to simply call format!("{} I am a {}", self.code(), self.crockery_name()), then you wouldn't need to worry about bases; using format!("{:x}", status) would automatically use lower-hex for your code. Of course, if you wanted to insist the code would be decimal you could write format!("{:d} I am a {}", ...). And you could also query the Formatter for the current base to react to it in any way which you deem appropriate (which is basically what the number types will do).

Basically, we should stop thinking of "type specifiers" and think instead of "formatting flags", base being one (similar to width, and alignment, and so on). "Type specifiers" is not really necessary once you have dynamic dispatch through a formatting trait - the "type specifiers" concept it is a leftover from C which had no dynamic dispatch mechanism.

BTW, the stream IO in C++ also moved away from format specifiers due to the same reasoning - once you overload operator<< for some type, it is rather pointless to ask the programmer to manually specify the type again in a specifier. Of course C++ went and eliminated the format string altogether - I think Rust chose wisely to maintain the sprintf concept because dealing with different flags for different printed objects is a PITA in the C++ approach.

@pnkfelix
Copy link
Member

I like the rudimentary type checking. I like the option of giving a succinct, inline hint as to what kind of data you should expect to see plugged in at that spot (even if that hint is not strictly enforced, due to the design of the fmt API.)

(To be honest I probably would have gone a different route with the format! design and opened up all 52 upper- and lower-case characters in the ASCII alphabet as potential traits to implement, leaving their interpretation as type-specifiers in the hands of the people writing the particular format strings; maybe I am nuts. I guess once we have library macros I'll be free to do that in a 3rd party library, and I'll learn what pitfalls it has on my own.)

Back to the subject at hand: People who don't like using the type specifiers are free to just implement default and use {} everywhere, right? So is the goal of this ticket to just reduce the fmt API surface?

@pnkfelix
Copy link
Member

(I do realize that one can use the named parameters feature of format!, combined with some Hungarian notation, as a way to get inline hints of the types you'll see. But that still leaves out the rudimentary type checking, which I see as part of the bargain here.)

@orenbenkiki
Copy link
Author

Type checking isn't bad as of itself (obviously :-) but the existing type specifiers are doing a double duty, one of the type check and another of controlling the base of printed integers; and they don't do a very good job at either role.

Today, for type checking, (1) one must implement both Default and SomeTypedTrait for each type, to allow using users to use either {} or {:type-check} (for some value of type-check); and (2) the set of type-checks is pretty arbitrary; one can distinguish between, say, numbers and booleans, but one can't distinguish between Foo objects and Bar objects (unless one artificially maps Foo to one existing type specifier and Bar to another).

For control over base of integer, one must associate the type with a specific base trait, thereby forcing anything that allows control over the base of integers to be grouped in the same "type" as all the numbers in the system. And one needs to associate the type with each and every base trait, without any compiler support for ensuring one did so, or put the burden over the user to know which base(s) are supported and which aren't.

It would be cleaner to not mix up the issue of base and the issue of type checking, and do each of these "properly". So how about this:

For type checking, allow {:b}, {:o}, {:d}, {:x}, {:X}. Consider allowing {:base(7)} / {:base(*)} / {:BASE(7)} / {:BASE(*)} if one wants to go overboard and allow "any" base. Any one of these would set the base member of the Formatter as proposed above.

For type checks, allow {:s}, {:i}, {:u} and others for the built-in types (finite list of type specifiers to match the finite list of built-in types), and also allow {:type(Foo)} for specifying formatting of any used-defined type.

Allow combining both base and type checks, e.g. {:ix} or {:d:u} or {:o:type(Foo)}.

This would be:

  • Backward-compatible with existing format strings.
  • Reduce repeated code by only requiring the implementation of one format trait per type, and making it easy to use the same code for formatting user-defined types regardless of the chosen base for printing integers.
  • Allow for safer format strings for both built-in types and user-defined types. For example, {:ux} would insist on printing an unsigned int in hexadecimal - today if one uses {:x} one loses the ability to specify the unsigned int check; and using {:type(Foo)} would insist one is printing a Foo value, which today isn't really possible.
  • If we want we could support {:base(*)} / {:BASE(*)} to allow for arbitrary bases (if that is considered useful).

Seems like a win-win...

@rust-highfive
Copy link
Contributor

This issue has been moved to the RFCs repo: rust-lang/rfcs#297

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants