New format! syntax has rough edges

Clippy as of Rust 1.67 is now encouraging us to rewrite expressions of the form …

println!("The thing is {}", thing);

… as …

println!("The thing is {thing}");

While we like this new syntax, we're finding it limiting that it can only reference existing variables and not any form of expression.

So, although the new Clippy warning nudges us toward a shorter syntax and team-wide consistency in formatting how we print variables, it nudges us toward inconsistency overall because we can't use that same form for other expressions.

Is this on the radar of the Rust language team? If not, how would you suggest we start that discussion?

5 Likes

+1
Also, VS Code does not understand that a variable inside a string is a variable, so it may be beautiful, but absolutely inconvenient. It's better to drop this recommendation.

1 Like

Note that Rust has always supported named format arguments; the thing that is newer is implicitly obtaining them from variables. Thus, if you want, you can be consistent in naming everything:

let a = 10;
let b = 20;
println!("{a} * {b} = {ab}", ab = a * b);

This is the style I've been using, and I find it comfortable. You can also of course bind expressions to variables with let preceding the formatting:

let a = 10;
let b = 20;
let ab = a * b;
println!("{a} * {b} = {ab}");

It is unlikely that expressions will ever appear inside of format strings, because that means parsing arbitrary Rust syntax out of what is, fundamentally, a string literal — note that Rust, unlike some other languages, does not have a special type of literal for formatting, but only macros. This would complicate the job of everything that wishes to analyze Rust programs — including the compiler.

25 Likes

I wonder, will these compile to the same machine code? Will they be similarly performant?

Yes, they are pretty much exactly the same. String formatting is already somewhat inefficient; named vs. numbered arguments won't make a difference. (But as always, you can just benchmark it rather than speculate.)

1 Like

Note that this is very intentional. Putting arbitrary expressions in strings is not a good thing, it's confusing and way too much complexity to be worth the (perceived) advantages.

(This came up previously, and I said pretty much the same thing back then.)

7 Likes

At the end of compilation, format strings (and let variables too) are long gone from the structure of the program and many optimization passes have happened. There is no name lookup at runtime.

5 Likes

For arbitrary expressions, definitely not. I don't think there's value in stuffing a whole match into the format string, for example. C# supports this, and stuffing ternary operators with other string literals into another string results in a mess, both for the tokenization rules and for humans.

For certain restricted things, maybe. It reminds me of the struct literal shorthand, which has had similar discussions -- people have talked about Foo { x: &x } maybe being just Foo { &x }, say, or Foo { a: x.a } maybe being just Foo { x.a }.

That said, the reference version isn't needed for formatting, since everything's automatically by reference there. And something like

format!("({self.a}, {self.b}, {self.c})")

has the pretty nice alternative of

let Self { a, b, c } = self;
format!("({a}, {b}, {c})")

which is only slightly longer overall and I personally find makes the format string itself easier to grok.

Thus this is one of those places (like lifetime elision was), where if someone wanted to make something happen, they should go do a comprehensive survey of how people are using formatting, and which patterns occur with enough frequency and prevalence to be worth simplifying, assuming those can be done without substantial readability impact.

14 Likes

I'm all for not allowing expressions there. It would lead to a horrible tangled mess.

8 Likes

Allowing all expressions is definitely a no-go. But extending current grammar to allowing place expression might be a good idea imo.

This was a deliberate design decision to support only the most basic identifiers and nothing else.

https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html#alternative-solution---interpolation

7 Likes

Hmm, what if named parameters could be bound inline by full patterns?

format!("({a}, {b}, {c})", Self { a, b, c } = self)
2 Likes

There will always be never ending bikeshedding on such a complex topic. Downside of the RFC process - any features with no clear, obvious "right way" either never happens, or is done the wrong way and ends up obtuse and hardly useable.

Fortunately rust has a really robust macro system and a great community of package authors.

This thread should tell any reader this feature will never land. Inthe meantime if there's anyone who cares about a practical solution, there's lots of crates which attempt it - my personal favorite is fmtools, which I think solves all the problems in this space - generality, powerful syntax, support for out of the box syntax highlighting.

Btw this discussion has highly derailed from the OP. Clippy is highly opinionated and often makes bad suggestions. Don't use or use sparingly. But it isn't strictly wrong to make this suggestion, as "idiomatic" is typically a matter of opinion.

2 Likes

16 posts were split to a new topic: When should code be tweaked to comply with Clippy lints?

Hardly seems worth it, IMHO. It's only saving a let (the , turns into a ;) so that added macro matcher complexity seems meh.

(TBH, given a time machine I'd be tempted to remove the x = a * b support from format! in favour of just having people use let. After all, one can write let (x, y, z) = (a * b, a + b, a - b); format!("{x}, {y}, {x}) instead of putting x = a*b, y = a+b, z = a - b in the format! call. And simplifying the format that way makes it much easier to understand how to indent the macro and such.)

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.