Propose implicit named arguments for formatting macros by davidhewitt · Pull Request #2795 · rust-lang/rfcs (original) (raw)

@CAD97

Most languages with string interpolation have a special case for single Ident

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 f"{ident}" or "{}".format(ident), but "{ident}".format() is meaningless. This RFC fuses string formatting and interpolation, so the experiences from other languages are not reliable, e.g. they can't express format!("{a} {b}", b=123).

And hence I disagree with this criticism:

Second: allowing arbitrary expressions opens a whole separate can of worms around nesting. Just as a few edge case examples: "{("a")}", "{(\"a\")}", r#"{(")}")}"#. You can make rules for how these resolve (it'd have to be "standard string token first, then work on the content of the string literal"), but this adds a huge number of edge cases that just plain don't exist for the ""{name}" is a shortcut for "{name}", name=name" simple case.

"{("a")}" is simply syntax error. Remember we're talking about a macro format!("...") here, the format string literal is not special in the Rust tokenizer/parser, it is not an interpolated string. To the proc macro format_args!(), the format strings "{(\")}\")}" and r#"{(")}")}"# are equivalent. The format string parser could,

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 {ident} is a named argument or interpolated variable!

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 {ident} is always a named argument, which leads to

this is backwards compatible to add on top of the "single Ident special case".

Indeed we accepted this RFC as-is, {ident:?} can also be forward extended to {(expr):?}. But you'll need to consider what to do with {expr:?} — either reject all non-ident expressions (thus {self.prop:?} is invalid), or accept some expressions (which leads to "why is my particular expression not accepted?" and true edge cases involving numbers and : and {). So I'd avoid even allowing that in the first place.


It's also by far the most common case.

Yes. But stuff like self.stuff or path.display() is also quite common. Grepping format!, panic! and (e)print(ln)! (including docs) from https://github.com/BurntSushi/ripgrep we get

Type Count
Single ident (e.g. format!("error: {}", err)) 147
Properties (e.g. format!("value {}", self.x.y.z) 22
Other expressions (e.g. format!("{}", xyz[0].replace("\n", "\n\n"))) 61
Expression with return branch (e.g. println!("{}", foo()?);) 2
Expression involving macros (e.g. format!("{} {}", $e, crate_version!())) 3

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.

https://github.com/BurntSushi/ripgrep/blob/8892bf648cfec111e6e7ddd9f30e932b0371db68/tests/util.rs#L403-L414

// 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");

https://github.com/BurntSushi/ripgrep/blob/8892bf648cfec111e6e7ddd9f30e932b0371db68/ignore/src/lib.rs#L287-L299

// 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())}")

https://github.com/BurntSushi/ripgrep/blob/8892bf648cfec111e6e7ddd9f30e932b0371db68/src/search.rs#L244-L264

// 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 ")