rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Extend format_args implicit arguments to allow field access

Open joshtriplett opened this issue 1 year ago • 32 comments

This RFC extends the "implicit named arguments" mechanism to allow accessing field names with var.field syntax: format!("{self.x} {var.another_field}").

Note that this does not permit .await, despite the syntactic similarity; it only allows field accesses.

Rendered

joshtriplett avatar May 06 '24 10:05 joshtriplett

Why wasn't this in the original RFC? Were there concerns or was it just a case of being cautiously minimalistic?

ChrisDenton avatar May 06 '24 10:05 ChrisDenton

@ChrisDenton Yes. See https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html#alternative-solution---interpolation and also the Future Possibilities section of 2795.

kennytm avatar May 06 '24 10:05 kennytm

does there need to be a discussion about the reaction to .await (and any other future postfix keywords / macros / etc)?

fbstj avatar May 06 '24 11:05 fbstj

@ChrisDenton Yes. See https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html#alternative-solution---interpolation and also the Future Possibilities section of 2795.

Thanks! Maybe I'm missing something but there doesn't appear to be any discussion on that in this RFC? I only see a terse reference to it in the disadvantages section?

Also there's no mention of the exploration of alternative syntax mentioned in rfc2795's Future Possibilities. Even if that's not being considered in this RFC, it would be nice to at least acknowledge that.

ChrisDenton avatar May 06 '24 12:05 ChrisDenton

I think allowing .await in format arguments is a bad idea because .await commonly has highly non-trivial side effects that are obscured by being in a format string, Deref isn't really a problem because it's already bad style for Deref to have significant side effects. Also, allowing .await just because it kinda looks like field access is imho an overgeneralization, it is nothing like field access.

programmerjake avatar May 06 '24 16:05 programmerjake

One property that current format strings have is that nothing in the format string can typically have side effects (other than a strange Debug/Display impl). Allowing .await would break this property. Maybe that's fine, but it should at least be noted. (Technically a Deref impl could also have a side effect, but as with Debug/Display that's atypical.)

Jules-Bertholet avatar May 06 '24 17:05 Jules-Bertholet

Also of the opinion that await should be disallowed, but that Deref is probably fine, although…

With Deref, there is the chance of interior mutability being exploited, and this can definitely result in some unexpected errors. For example, calling Deref on a Ref from a RefCell could induce a panic, and similarly there could be other cases where Deref does some meaningful calculation, even if they don't have to. It would be unprecedented, but perhaps we could require that the types implementing Deref have no interior mutability (Freeze) and require those specifically to be effectively pure. Of course, this would mean that Freeze is an API contract, which I know folks have been trying to avoid.

Or, if we decide against that, I would strongly recommend just making the evaluation order explicitly linear, with Deref being called each time as if you had a series of let statements. This feels the most reasonable and intuitive.

clarfonthey avatar May 06 '24 18:05 clarfonthey

With Deref, there is the chance of interior mutability being exploited, and this can definitely result in some unexpected errors.

this is exactly the same as accessing interior mutability in Display...you can run whatever code you like but people expect the implementations to have no effect other than writing the output or, for Deref, returning the reference

For example, calling Deref on a Ref from a RefCell could induce a panic

it can't panic actually, it is only a call to NonNull::as_ref which is just a pointer dereference.

, and similarly there could be other cases where Deref does some meaningful calculation, even if they don't have to. It would be unprecedented, but perhaps we could require that the types implementing Deref have no interior mutability (Freeze) and require those specifically to be effectively pure

there are ways around that (Box<Cell<T>>) and I don't think we should restrict which types you can use. a common type that has interior mutability and Deref is lazily-initialized statics (lazy_static/LazyCell/LazySync), which I expect to work in format_args if field accesses are permitted at all.

programmerjake avatar May 06 '24 18:05 programmerjake

I just found out that format_args!("{var}") automatically turns all keywords into raw identifiers which leads to rust-lang/rust#115466. So should the 3rd or 4th or 5th assert_eq below work?

#[derive(Debug)]
struct Await {
    r#await: u32,
}

fn main() {
    let r#await = Await { r#await: 6 };
    assert_eq!(format!("{await:?}"), "Await { await: 6 }".to_string()); // <-- currently compiles!
//  assert_eq!(format!("{r#await:?}"), "Await { await: 6 }".to_string()); // <-- error, see #115466
    assert_eq!(format!("{await.await}"), "6".to_string()); // ?
    assert_eq!(format!("{await.r#await}"), "6".to_string()); // ??
    assert_eq!(format!("{r#await.r#await}"), "6".to_string()); // ???
}

kennytm avatar May 06 '24 21:05 kennytm

EDIT: Note that I've now removed .await from the RFC. This comment was made before that point.

I think allowing .await in format arguments is a bad idea because .await commonly has highly non-trivial side effects that are obscured by being in a format string, Deref isn't really a problem because it's already bad style for Deref to have significant side effects. Also, allowing .await just because it kinda looks like field access is imho an overgeneralization, it is nothing like field access.

The rationale for limiting expression types in format strings isn't to limit side effects, it's purely a matter of syntax, and what syntaxes create confusion within a format string. On that basis, allowing anything that uses . seems fine.

If we ever added arbitrary expressions in the future, we'd want some kind of delimiter (e.g. a required set of braces or parentheses). I don't know that we want to do that, but I think that'd be a minimum requirement if we did. The rationale for this RFC is that we can add expressions that use . without needing extra delimiters like those. That's based purely on syntax, not semantics.

joshtriplett avatar May 07 '24 08:05 joshtriplett

The rationale for limiting expression types in format strings isn't to limit side effects, it's purely a matter of syntax, and what syntaxes create confusion within a format string. On that basis, allowing anything that uses . seems fine.

If we ever added arbitrary expressions in the future, we'd want some kind of delimiter (e.g. a required set of braces or parentheses). I don't know that we want to do that, but I think that'd be a minimum requirement if we did. The rationale for this RFC is that we can add expressions that use . without needing extra delimiters like those. That's based purely on syntax, not semantics.

The problem is that this encourages restricting features added to the language to a particular kind of syntax, or adding custom syntax to support certain operations. If await is accessible just as a field, why can't we add in property functions without parentheses like Python or Kotlin? Why are tuples allowed to treat number indices as fields but not arrays? etc.

If the dot access is simple enough, why aren't method calls with no arguments simple enough? etc.

It just feels like opening the door to specific kinds of syntax that "feel" okay adds a bad incentive to make syntax "simpler," at the potential cost of clarity.

clarfonthey avatar May 07 '24 17:05 clarfonthey

Yeah as I've mentioned in #2795 perhaps it's better to go all-in supporting {(expr)}. You'll need to deal with side-effect due to custom Deref anyway.


BTW #2795's Future possibilities section also suggested using {(expr)} even if expr are only field access:

Future discussion on this topic may also focus on adding interpolation for just a subset of possible expressions, for example dotted.paths. We noted in debate for this RFC that particularly for formatting parameters the existing dollar syntax appears problematic for both parsing and reading, for example {self.x:self.width$.self.precision$}.

The conclusion we came to in the RFC discussion is that adding even just interpolations for dotted.paths will therefore want a new syntax, which we nominally chose as the {(expr)} syntax already suggested in the interpolation alternative section of this RFC.

Using this parentheses syntax, for example, we might one day accept {(self.x):(self.width).(self.precision)} to support dotted.paths and a few other simple expressions. The choice of whether to support an expanded subset, support interpolation of all expressions, or not to add any further complexity to this macro is deferred to the future.

Using {(expr)} can also resolve the raw-identifier issue in https://github.com/rust-lang/rfcs/pull/3626#issuecomment-2096994642 — you must write {(r#await.r#await)} because the content is parsed as an actual Rust expression, rather than cooked identifiers separated by dots.

kennytm avatar May 07 '24 18:05 kennytm

I like (expr) a lot because it makes things substantially easier to read and understand when used to configure formatting parameters, rather than adding in extra $ which could be confused as having semantic meaning.

One concern I'd have with fully supporting (expr) is how rustfmt should handle these expressions nested inside formatting strings. Presumably, you'd just treat it like any other nested expression:

println!("Hello {(match self.pref(person) {
    Pref::Like => "friend",
    Pref::Neutral => "there",
    Pref::Dislike => "you piece of trash"
})}!");

And of course, there could be some confusion with {{expr}}, which would just print the actual code inside the braces, instead of the result. Of course, this would probably be mostly alleviated by syntax highlighting, since I'd expect IDEs to syntax-highlight the code inside the parentheses if it's valid.

Another way would also be to permit spaces around the formatting string itself within the braces, which feels like it'd be reasonable to have:

println!("{greeting} { (person.await) }!")

And we could also make this the default for rustfmt when any expression is used inside the braces. This feels ideal since I'm certain that people would like to be able to use {{ and }} to print actual code and wouldn't want to have to selectively deal with warnings depending on whether the code represents Rust syntax.

clarfonthey avatar May 08 '24 22:05 clarfonthey

println!("Hello {(match self.pref(person) {
    Pref::Like => "friend",
    Pref::Neutral => "there",
    Pref::Dislike => "you piece of trash"
})}!");

Unless Rust added something like JavaScript's template strings (which is probably the best way to get something like this to work), the quotes inside the format string need additional escaping.

programmerjake avatar May 08 '24 22:05 programmerjake

println!("Hello {(match self.pref(person) {
    Pref::Like => "friend",
    Pref::Neutral => "there",
    Pref::Dislike => "you piece of trash"
})}!");

Unless Rust added something like JavaScript's template strings (which is probably the best way to get something like this to work), the quotes inside the format string need additional escaping.

Right, you'd probably want to use raw strings here instead, which sidesteps the issue. Although it feels like the parser should be able to recover here and suggest that.

clarfonthey avatar May 08 '24 23:05 clarfonthey

Wearing my "just another Rust user" hat, this RFC would be nice!

On Thu, May 9, 2024, at 1:09 AM, Clar Fon wrote:

println!("Hello {(match self.pref(person) { Pref::Like => "friend", Pref::Neutral => "there", Pref::Dislike => "you piece of trash" })}!"); Unless Rust added something like JavaScript's template strings (which is probably the best way to get something like this to work), the quotes inside the format string need additional escaping.

Right, you'd probably want to use raw strings here instead, which sidesteps the issue. Although it feels like the parser should be able to recover here and suggest that.

— Reply to this email directly, view it on GitHub https://github.com/rust-lang/rfcs/pull/3626#issuecomment-2101642330, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABF4ZQYAXOHR3E5BEECDC3ZBKWEFAVCNFSM6AAAAABHIYOV4KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMBRGY2DEMZTGA. You are receiving this because you are subscribed to this thread.Message ID: @.***>

nikomatsakis avatar May 10 '24 12:05 nikomatsakis

A very common thing I do is formatting self.something in custom Display implementations. Simply for that, this would be amazing.

jdonszelmann avatar May 10 '24 15:05 jdonszelmann

A very common thing I do is formatting self.something in custom Display implementations. Simply for that, this would be amazing.

That's exactly the use case that motivated me to write this in the first place. :)

joshtriplett avatar May 11 '24 15:05 joshtriplett

FWIW I use formatting frequently to print the length of lists or do some internal formatting, and it'd be nice to support simple method calls

print!("{list.len()}"
print!("{date.utc()}"

Not sure how that would interact with the RFC but I'd get way more mileage out of that than just field access. Raw exprs would be ideal of course.

This particular method suggestion starts to get hairy when you have arguments to those method calls since those are technically exprs. Not sure how to resolve it without having some weird inconsistency in what can and can't be formatted.

print!("{date.utc(arg)}"

jkelleyrtp avatar May 17 '24 23:05 jkelleyrtp

A very common thing I do is formatting self.something in custom Display implementations. Simply for that, this would be amazing.

FWIW, in stable Rust I destructure self. This frequently lets the format macro be smaller (and IMO more readable):

format!("{} jumps over {}", self.source, self.target);

// Current Rust
let Self { source, target } = self;
format!("{source} jumps over {target}");

// Alternate stable version
format!("{source} jumps over {target}", source = self.source, target = self.target);

// RFC proposed
format!("{self.source} jumps over {self.target}");

shepmaster avatar May 18 '24 20:05 shepmaster

it'd be nice to support simple method calls

I do not wish to prevent this RFC from moving forward, but I'd also like this as a next step. A common case for me is printing Paths / PathBufs in error values (shown here using SNAFU's syntax):

#[snafu(display("Could not delete the crate file {}", path.display()))]

shepmaster avatar May 18 '24 20:05 shepmaster

Misc additional motivation: I often currently avoid using format_args implicit args because the overhead of switching between format!("foo {}", foo.bar) and format!("foo {foo}") depending on whether there happens to be a field access pushes me to just always use explicit args. I would love to instead switch my default to be implicit args, which this RFC would enable :)

illicitonion avatar Aug 17 '25 22:08 illicitonion

There's this strange behavior that needs to be resolved before this RFC goes through, I think. https://github.com/rust-lang/rust/issues/145739

theemathas avatar Aug 22 '25 03:08 theemathas

I've now removed .await from the RFC. While syntactically similar, it's simultaneously more complex, more controversial, and much less important than field access. Remove it, in the hopes of unblocking the much more important field access.

I've also added more discussion of arbitrary expressions, and why this RFC proposes adding field accesses without further syntactic weight (e.g. parentheses) whether or not we added arbitrary expressions in the future.

joshtriplett avatar Aug 25 '25 05:08 joshtriplett

What's the intended behavior for fields with names that step on keywords? E.g.:

#[derive(Default)]
struct S { r#type: () }
let s = S::default();
println!("{s.type}");

(If the intention is to give an error, would we accept {s.r#type}?)

If we accept this, it certainly closes a door. On the other hand, it might be strange not to allow this given that we otherwise treat identifiers in format strings as raw.


This syntax only permits referencing fields from identifiers in scope. It does not permit referencing fields from named arguments passed into the macro. For instance, the following syntax is not valid, and results in an error:

println!("{x.field}", x=expr()); // Error

Why not accept this? It seems maybe worth minimizing the differences in behavior if we can.

Giving an error might be particularly surprising as we otherwise accept, e.g.:

let x = 0u8;
println!("{x}", x=x);

And, as discussed over in https://github.com/rust-lang/rust/issues/145739, that might be the model for the desugaring. Given:

#[derive(Debug)]
struct LoudDrop;

impl Drop for LoudDrop {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn main() {
    println!("{LoudDrop:?}, {LoudDrop:?}"); //~ 1.
    println!("{LoudDrop:?}, {LoudDrop:?}", LoudDrop=LoudDrop); //~ 2.
    println!("{:?}, {:?}", LoudDrop, LoudDrop); //~ 3.
}

It's true today that 1 behaves the same as 2 and differently than 3.


Speaking of desugaring, it'd be worth specifying that in this RFC. Given:

use core::ops::Deref;

struct W<T>(T);

impl<T> Drop for W<T> {
    fn drop(&mut self) {
        println!("drop");
    }
}

impl<T> Deref for W<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        println!("deref");
        &self.0
    }
}

struct S<T> { f: T }

fn main() {
    const X: W<S<u8>> = W(S { f: 0 });
    println!("{X.f} {X.f}"); //~ Format string to desugar.
    println!("-- 1:");
    println!("{} {}", X.f, X.f); //~ Desugaring 1.
    println!("-- 2:");
    { let x = X; println!("{} {}", x.f, x.f) }; //~ Desugaring 2.
    println!("-- 3:");
    println!("{f} {f}", f=X.f); //~ Desugaring 3.
}

Which desugaring and behavior do we expect?

traviscross avatar Aug 25 '25 06:08 traviscross

From discussion in today's @rust-lang/lang meeting: I'm going to add something to the alternatives section to explicitly discuss and explain why we don't want to reject Deref.

EDIT: Done.

joshtriplett avatar Nov 05 '25 19:11 joshtriplett

@traviscross and I are going to talk through the desugaring, and the handling of keywords and raw keywords, and I'll update the RFC accordingly.

joshtriplett avatar Nov 05 '25 19:11 joshtriplett

@traviscross and I discussed this, and I'm going to update the RFC to be more precise about desugaring and some corner cases.

joshtriplett avatar Nov 13 '25 20:11 joshtriplett

Cc:

  • https://github.com/rust-lang/rust/issues/145739

traviscross avatar Nov 13 '25 23:11 traviscross

See https://github.com/rust-lang/rust/issues/145739#issuecomment-3530192688 for details that arose as a result of exploring the correct desugaring here.

If we end up fixing that, we can adjust the RFC accordingly. The RFC as specified is consistent with our current behavior for implicit named arguments. I've added this as an unresolved question to address before stabilization.

joshtriplett avatar Nov 14 '25 02:11 joshtriplett