Propose implicit named arguments for formatting macros by davidhewitt · Pull Request #2795 · rust-lang/rfcs (original) (raw)
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:
- Python's f-string
f"{expr}"
- C#/VB's interpolated strings
$"{expr}"
- JavaScript/TypeScript's template literals
`${expr}`
- Swift's string interpolation
"\(expr)"
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
- Kotlin, Groovy, PHP:
"$ident ${expr}"
- Ruby:
"#@xxx #@@yyy #$zzz #{expr}"
(note: local variables (no sigils) won't work, must use"#{ident}"
) - Scala:
s"$ident ${expr}"
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,
- If we see
{
,- If we see
{
, unescape as{
- If we see an integer, it's a positional argument
- If we see an identifier, it's a named argument
- If we see
:
or}
, it's an (implicit) positional argument - If we see
(
, step back one character then parse a Rust token tree - Otherwise, emit an error
- If we see
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.
// 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 ")