Skip to content
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

Extend format_args implicit arguments to allow field access #3626

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
202 changes: 202 additions & 0 deletions text/3626-format-args-implicit-dot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
- Feature Name: `format_args_implicit_dot`
- Start Date: 2023-10-01
- RFC PR: [rust-lang/rfcs#3626](https://github.com/rust-lang/rfcs/pull/3626)
- Rust Issue: [rust-lang/rust#00000](https://github.com/rust-lang/rust/issues/00000)

# Summary
[summary]: #summary

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

# Motivation
[motivation]: #motivation

[RFC 2795](https://github.com/rust-lang/rfcs/pull/2795) added "implicit named
arguments" to `std::format_args!` (and other macros based on it such as
`format!` and `println!` and `panic!`), allowing the format string to reference
variables in scope using identifiers. For instance, `println!("Hello {name}")`
is now equivalent to `println!("Hello {name}", name=name)`.

The original implicit named arguments mechanism only permitted single
identifiers, to avoid the complexity of embedding arbitrary expressions into
format strings. The implicit named arguments mechanism is widely used, and one
of the most common requests and most common reasons people cannot use that
syntax is when they need to access a struct field. Adding struct field syntax
does not conflict with any other format syntax, and unlike allowing *arbitrary*
expressions, allowing struct field syntax does not substantially increase
complexity or decrease readability.

This proposal has the same advantages as the original implicit named arguments
proposal: making more formatting expressions easy to read from left-to-right
without having to jump back and forth between the format string and the
arguments.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

With this proposal accepted, the following (currently invalid) macro
invocation:

```rust
format_args!("hello {person.name}")
```

would become a valid macro invocation, and would be equivalent to a shorthand
for the already valid:

```rust
format_args!("hello {unique_ident}", unique_ident=person.name)
```

The identifier at the beginning of the chain (`person` in this case) must be an
identifier which existed in the scope in which the macro is invoked, and must
have a field of the appropriate name (`name` in this case).

This syntax works for fields within fields as well:

```rust
format_args!("{obj.field.nested_field.another_field}")
```

As a result of this change, downstream macros based on `format_args!` would
also be able to accept implicit named arguments in the same way. This would
provide ergonomic benefit to many macros across the ecosystem, including:

- `format!`
- `print!` and `println!`
- `eprint!` and `eprintln!`
- `write!` and `writeln!`
- `panic!`, `unreachable!`, `unimplemented!`, and `todo!`
- `assert!`, `assert_eq!`, and similar
- macros in the `log` and `tracing` crates

(This is not an exhaustive list of the many macros this would affect.)

## Additional formatting parameters

As a result of this RFC, formatting parameters can also use implicit named
argument capture:

```rust
println!("{self.value:self.width$.self.precision$}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?
Not sure how it follows from the rest of the RFC.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

println!("{0:1$.2$}", self.value, self.width, self.precision);

follows trivially from the definition of argument in the fmt grammar

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I have never seen the name$ syntax before, but apparently it's not even a new feature, works since 1.0.

```

This is slightly complex to read, but unambiguous thanks to the `$`s.

## `await`

Formatting can use `.await`, as well:

```rust
println!("{future1.await} {future2.await}");
joshtriplett marked this conversation as resolved.
Show resolved Hide resolved
```

## Compatibility

This syntax is not currently accepted, and results in a compiler error. Thus,
adding this syntax should not cause any breaking changes in any existing Rust
code.

## No field access from named arguments

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:

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

If there is an ambiguity between an identifier in scope and an identifier used
for a named argument, the compiler emits an error.

```rust
let x = SomeStruct::new();
println!("{x.field}", x=expr()); // Error
```

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The implementation captures the first identifier in the chain using the same
mechanism as implicit format arguments, and then uses normal field accesses to
obtain the value, just as if the field were accessed within a named argument.
Thus, the following two expressions are semantically equivalent:

```rust
format_args!("{name.field1.field2}")

format_args!("{unique_identifier}", unique_identifier=name.field1.field2)
```

Any `Deref` operations or `.await` operations associated with the `.` in each
format argument are evaluated from left-to-right as they appear in the format
string, at the point where the format string argument is evaluated, before the
positional or named arguments are evaluated.

If the identifier at the start of the chain does not exist in the scope, the
usual error E0425 would be emitted by the compiler, with the span of that
identifier:

```
error[E0425]: cannot find value `person` in this scope
--> src/main.rs:X:Y
|
X | format_args!("hello {person.name}");
| ^^^^^^ not found in this scope
```

If one of the field references refers to a field not contained in the
structure, the usual error E0609 would be emitted by the compiler, with the
span of the field identifier:

```
error[E0609]: no field `name` on type `person`
--> src/main.rs:X:Y
|
5 | format_args!("hello {person.name}");
| ^^^^ unknown field
```

# Drawbacks
[drawbacks]: #drawbacks

This adds incremental additional complexity to format strings.

Having `x.y` available may make people assume other types of expressions work
as well.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

The null alternative is to avoid adding this syntax, and let users continue to
pass named arguments or bind local temporary names rather than performing
inline field accesses within format strings. This would continue to be
inconvenient but functional.

This functionality could theoretically be implemented in a third-party crate,
but would then not be automatically and consistently available within all of
Rust's formatting macros, including those in the standard library and those
throughout the ecosystem.

We could omit support for other formatting parameters (width, precision).
However, this would introduce an inconsistency that people have to remember;
people would *expect* this to work.

We could omit support for `.await`. However, to users this may seem like an
arbitrary restriction.

# Prior art
[prior-art]: #prior-art

Rust's existing implicit format arguments serve as prior art, and discussion
around that proposal considered the possibility of future (cautious) extension
to additional types of expressions.

The equivalent mechanisms in some other programming languages (e.g. Python
f-strings, Javascript backticks, C#, and various other languages) allow
arbitrary expressions. This RFC does *not* propose adding arbitrary
expressions, nor should this RFC serve as precedent for arbitrary expressions,
but nonetheless these other languages provide precedent for permitting more
than just single identifiers.