-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Propose implicit named arguments for formatting macros #2795
Propose implicit named arguments for formatting macros #2795
Conversation
An alternative syntax is to clearly distinguish the interpolated variable from named arguments, println!("Hello, {(person)}!");
// ^ ^ and it allows all expressions can be naturally used format_args!("Hello {(get_person())}")
// ^~~~~~~~~~~~~^
println!(r#"{("Hello"):~^33}\n{(self.person):~^33}"#);
println!("Hello, {(if self.foo { &self.person } else { &self.other_person })}");
println!(
"variable-person = {(person1)}, named-person = {person2}, positional-person = {}",
person3,
person2=person2,
); |
First: this is backwards compatible to add on top of the "single Ident special case". Most languages with string interpolation have a special case for single Ident, so we wouldn't be out of line to make that easier. It's also by far the most common case. Second: allowing arbitrary expressions opens a whole separate can of worms around nesting. Just as a few edge case examples: So we definitely should stick to the simple, fairly obvious case that eliminates that stutter, and worry about generalizing it to more expressions later if desired. |
Half of the languages with string interpolation have special case for single ident. Firstly, many of them do support interpolating arbitrary expressions, without any special case for single idents:
Furthermore, when the string interpolation syntax is special-cased for single idents, it is often because there is no closing sigil, which is not the case for Rust
Finally, string interpolation is a feature built into the language itself, which often live independently from the "format" facility, e.g. in Python you write And hence I disagree with this criticism:
I don't see "huge number of edge cases" syntactically. IMO it has less edge cases than the current RFC because when you see an ident, you don't need to determine whether And to clarify, introducing the "stutter" is the primary purpose of my comment above even if we reject interpolating arbitrary expressions. It's to ensure
Indeed we accepted this RFC as-is,
Yes. But stuff like
So non-single-ident covers 40% of all usages, which I won't brush them off simply as can-of-worms opener. Also, when these expressions are interpolated, they are quite readable unlike the RFC's constructed example.// Current
panic!("\n\n==========\n\
command failed but expected success!\
{}\
\n\ncommand: {:?}\
\ncwd: {}\
\n\nstatus: {}\
\n\nstdout: {}\
\n\nstderr: {}\
\n\n==========\n",
suggest, self.cmd, self.dir.dir.display(), o.status,
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr));
// Interpolated
panic!("\n\n==========\n\
command failed but expected success!\
{(suggest)}\
\n\ncommand: {(self.cmd):?}\
\ncwd: {(self.dir.dir.display())}\
\n\nstatus: {(o.status)}\
\n\nstdout: {(String::from_utf8_lossy(&o.stdout))}\
\n\nstderr: {(String::from_utf8_lossy(&o.stdout))}\
\n\n==========\n"); // Current
write!(f, "{}", msgs.join("\n"))
...
write!(f, "File system loop found: \
{} points to an ancestor {}",
child.display(), ancestor.display())
// Interpolated
write!(f, r#"{(msgs.join("\n"))}"#)
...
write!(f, "File system loop found: \
{(child.display())} points to an ancestor {(ancestor.display())}") // Current
write!(
self.get_mut(),
"
{matches} matches
{lines} matched lines
{searches_with_match} files contained matches
{searches} files searched
{bytes_printed} bytes printed
{bytes_searched} bytes searched
{search_time:0.6} seconds spent searching
{process_time:0.6} seconds
",
matches = stats.matches(),
lines = stats.matched_lines(),
searches_with_match = stats.searches_with_match(),
searches = stats.searches(),
bytes_printed = stats.bytes_printed(),
bytes_searched = stats.bytes_searched(),
search_time = fractional_seconds(stats.elapsed()),
process_time = fractional_seconds(total_duration)
)
// Interpolated
write!(
self.get_mut(),
"
{(stats.matches())} matches
{(stats.matched_lines())} matched lines
{(stats.searches_with_match())} files contained matches
{(stats.searches())} files searched
{(stats.bytes_printed())} bytes printed
{(stats.bytes_searched())} bytes searched
{(fractional_seconds(stats.elapsed())):0.6} seconds spent searching
{(fractional_seconds(total_duration)):0.6} seconds
") |
As a comparison point, there is a substantial difference between the mentioned In the linked |
I think @kennytm's interpolation proposal is reasonable, and I will add it to the alternatives section in the RFC shortly. My initial observations about the proposed syntax for interpolation are that it's not actually any shorter than positional formatting arguments, although I would argue it reads easier (especially as the number of arguments scales):
However, if the brackets are necessary to resolve parsing ambiguities then that is something we would have to accept. I do find iterpolation mechanisms convenient, though it can be difficult to judge how complex interpolations should be before they become hard-to-read / poor style. I would like to contest this line though:
The objective of this RFC is not to add interpolation to Rust's existing formatting macros. It is to add a shorthand to the macros to make them more ergonomic in a very common use case. I'd be glad to take a win on 60% of macro invocations even if this RFC doesn't improve the remaining 40%. This discussion appears to agree that interpolation can be added later in a backwards-compatible way (even if there are different arguments whether that would be desirable). As pointed out by @petrochenkov in the WIP PR, we actually already have a similar shorthand in struct literal expressions for the special case of single identifiers:
To make the original intention of the RFC stand out clearer, I will update the wording to state explicitly that adding interpolation to the macros was not the intended goal, as well as add the struct literal shorthand to the prior art section. |
The RFC itself is not intended to be a way to sneak interpolation into the formatting macros; the RFC author believes that they do not need full interpolation support, although would not rule it out if it was deemed desirable. This update to the RFC text clarifies the distinction between implicit named arguments and interpolation. It also adds a note on prior art that Field Init Shorthand is an existing precedent where language ergonomics have introduced a special case for single identifiers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the nuances you've added.
Could you also state whether format!("{x}, {y}", y = 42)
would be accepted or rejected? (it would avoid having let
declarations used only for one "fstring" interpolation)
I would expect it would be accepted: it seems perfectly valid to me that one named parameter can be implicit and the other explicit. |
I think there's value in separating the computing of results and how they are rendered (so that the former can be separated into functions eventually). With the Also, given that 60% of the cases in the data in question were for single-ident cases, it seems to me that The rustc compiler has a lot of diagnostics so it is probably a better data set to dig into if you want more of those. All in all, I think this RFC finds a well-struck balance. |
The examples provided had incorrectly merged Scala and PHP. Scala's string interpolation is written `s"$foo"` whereas PHP is written `"$foo"`
Would this involve any changes to the language syntax, or can this be implemented entirely in a macro? I realise that |
The fstrings crate implements a PoC of this functionality using Also, the test PR I wrote needed just a tiny adjustment to the internals of |
I'd love to have this. I find myself writing I fully support the rationale in this RFC to only support single identifiers, and defer any more complex interpolation. I do think we'll want dotted fields in the future, but I added a comment about one bit of complexity even that level of extended interpolation would produce. |
I have a couple of reasons to not support arbitrary expressions without require parenthesis (
|
Fully agree with your arguments. For anything beyond |
A new version of It can be useful for those wanting to get a feeling of what the ergonomic improvements this RFC and a future one could provide. |
Another possible alternative: No special case for identifiers, but allow arbitrary expressions within parenthesis. So, |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. The RFC will be merged soon. |
Very excited to see this given a chance! I'm happy to do the implementation work for this going forward. |
Checklist for the tracking issue: Steps:
Unresolved Questions:
|
Huzzah! The @rust-lang/lang and @rust-lang/libs teams have decided to accept this RFC. To track further discussion, subscribe to the tracking issue here: |
…varkor Add `format_args_capture` feature This is the initial implementation PR for [RFC 2795](rust-lang/rfcs#2795). Note that, as dicussed in the tracking issue (rust-lang#67984), the feature gate has been called `format_args_capture`. Next up I guess I need to add documentation for this feature. I've not written any docs before for rustc / std so I would appreciate suggestions on where I should add docs.
…varkor Add `format_args_capture` feature This is the initial implementation PR for [RFC 2795](rust-lang/rfcs#2795). Note that, as dicussed in the tracking issue (rust-lang#67984), the feature gate has been called `format_args_capture`. Next up I guess I need to add documentation for this feature. I've not written any docs before for rustc / std so I would appreciate suggestions on where I should add docs.
…nts-as-str, r=Amanieu Add Arguments::as_str(). There exist quite a few macros in the Rust ecosystem which use `format_args!()` for formatting, but special case the one-argument case for optimization: ```rust #[macro_export] macro_rules! some_macro { ($s:expr) => { /* print &str directly, no formatting, no buffers */ }; ($s:expr, $($tt:tt)*) => { /* use format_args to write to a buffer first */ } } ``` E.g. [here](https://github.com/rust-embedded/cortex-m-semihosting/blob/7a961f0fbe6eb1b29a7ebde4bad4b9cf5f842b31/src/macros.rs#L48-L58), [here](https://github.com/rust-lang-nursery/failure/blob/20f9a9e223b7cd71aed541d050cc73a747fc00c4/src/macros.rs#L9-L17), and [here](https://github.com/fusion-engineering/px4-rust/blob/7b679cd6da9ffd95f36f6526d88345f8b36121da/px4/src/logging.rs#L45-L52). The problem with these is that a forgotten argument such as in `some_macro!("{}")` will not be diagnosed, but just prints `"{}"`. With this PR, it is possible to handle the no-arguments case separately *after* `format_args!()`, while simplifying the macro. Then these macros can give the proper error about a missing argument, just like `print!("{}")` does, while still using the same optimized implementation as before. This is even more important with [RFC 2795](rust-lang/rfcs#2795), to make sure `some_macro!("{some_variable}")` works as expected.
…nts-as-str, r=Amanieu Add Arguments::as_str(). There exist quite a few macros in the Rust ecosystem which use `format_args!()` for formatting, but special case the one-argument case for optimization: ```rust #[macro_export] macro_rules! some_macro { ($s:expr) => { /* print &str directly, no formatting, no buffers */ }; ($s:expr, $($tt:tt)*) => { /* use format_args to write to a buffer first */ } } ``` E.g. [here](https://github.com/rust-embedded/cortex-m-semihosting/blob/7a961f0fbe6eb1b29a7ebde4bad4b9cf5f842b31/src/macros.rs#L48-L58), [here](https://github.com/rust-lang-nursery/failure/blob/20f9a9e223b7cd71aed541d050cc73a747fc00c4/src/macros.rs#L9-L17), and [here](https://github.com/fusion-engineering/px4-rust/blob/7b679cd6da9ffd95f36f6526d88345f8b36121da/px4/src/logging.rs#L45-L52). The problem with these is that a forgotten argument such as in `some_macro!("{}")` will not be diagnosed, but just prints `"{}"`. With this PR, it is possible to handle the no-arguments case separately *after* `format_args!()`, while simplifying the macro. Then these macros can give the proper error about a missing argument, just like `print!("{}")` does, while still using the same optimized implementation as before. This is even more important with [RFC 2795](rust-lang/rfcs#2795), to make sure `some_macro!("{some_variable}")` works as expected.
…pture, r=Mark-Simulacrum stabilize format args capture Works as expected, and there are widespread reports of success with it, as well as interest in it. RFC: rust-lang/rfcs#2795 Tracking issue: rust-lang#67984 Addressing items from the tracking issue: - We don't support capturing arguments from a non-literal format string like `format_args!(concat!(...))`. We could add that in a future enhancement, or we can decide that it isn't supported (as suggested in rust-lang#67984 (comment) ). - I've updated the documentation. - `panic!` now supports capture as well. - There are potentially opportunities to further improve diagnostics for invalid usage, such as if it looks like the user tried to use an expression rather than a variable. However, such cases are all already caught and provide reasonable syntax errors now, and we can always provided even friendlier diagnostics in the future.
The RFC doesn't document how it interacts with raw idents, so for posterity: let r#type = 5;
// Before this feature
assert_eq!("5", format!("{}", r#type));
assert_eq!("6", format!("{type}", type = 6)); // Named explicit does not need to be raw.
assert_eq!("5", format!("{type}", type = r#type));
assert_eq!("5", format!("{type}", r#type = r#type)); // Named explicit can be raw and maps to cooked version in the format string.
println!("{type}", type = type); // Error. RHS is an invalid expr.
println!("{r#type}", r#type = r#type); // Error. Named explicit can be raw but the format string can only use the cooked version.
// New after this feature
assert_eq!("5", format!("{type}")); // Format string references cooked ident and value is from raw ident.
// ie compiler is smart enough to treat this as `type = r#type` and not `type = type`
println!("{r#type}"); // Still an error. Format string can only use the cooked version.
// So `"{type}"` is the *only* way to write this. |
* Rust 1.58 onwards allows using implicit arguments in format! macro * see rust-lang/rfcs#2795
Updates format strings to use the implicit named arguments (`format_args_capture`) introduced in [RFC2795][1] whenever possible. | Before | After | | -------------------- | -------------------------------- | | `xxx!("{}", self.x)` | `xxx!("{}", self.x)` (no change) | | `xxx!("{}", x)` | `xxx!("{x}")` | [1]: rust-lang/rfcs#2795
Rendered
Proposes the ability to pass implicit named arguments to formatting macros, for example:
A follow up to internals discussion https://internals.rust-lang.org/t/println-use-named-arguments-from-scope/10633 and WIP PR rust-lang/rust#65338