Warn about using the value of an assignment expression?

Hi, just wondering if this would make sense as a FR, before I file it

let mut a;
let mut b;
a = b = 42;
dbg!(a, b);

play

Coming from C or Python, you might expect to end up with a = 42. It is documented that in Rust, the value of an assignment is ().

Clippy will at least warn about let_unit_value, which is nice, although it doesn't make it very obvious why this assigns () to a.

I wonder, would it make sense for Clippy (or even rustc?) to warn about any non-trivial use of the value of an assignment expression? Or if not that, perhaps I could send a PR to add some suggestion about this to Clippy's help for this lint?

4 Likes

Thats surprising. I would support that feature. Iā€™m also at a loss for what the value of an assignment expression would be the unit value. I could see for a non-copy type that b could end up moved into a, but not that a would be (). Is there some reason it must be so?

I think a rustc lint/warning would be appropiate here. Something similar to

warning: chaining assignment is not supported in rust
 --> src/main.rs:4:5
  |
4 |     a = b = 42
  |     ^^^^^^^^^^
  |
  = note: `#[warn(chaining_assignment)]` on by default
  = help: `a` has the unit value afterwards, which might be surprising

not sure about the "not supported', because it is, but not what you think it would be. Maybe somebody can come up with a better term

3 Likes

I don't mind "not supported" even if it technically is (supported); we could go for something along the lines of "does not do what you may expect"; but we get closer to clippy's lint (which, by the way, I find fine).

As to why it evaluates to (), it is because an assignment is a statement, which is being used in an expression context. It is thus a "statement expression", i.e., a "dummy" expression that thus evaluates to the conventional nothing, ().

This is required so that everything in Rust can be an expression, thus removing the need for things like the classic if-then-else statement and be able to use ternary operators everywhere (Rust's if-then-else).

2 Likes

It seems to me this is not really driven by Rust being an expression-oriented language. Assignment expressions have a result in C, and C is less expression-oriented than Rust.

I would be interested to read any early discussion about why it's defined as it is. My guess would be it is more driven by Rust having only some values that are Copy, and some that are explicitly dropped. An interesting both would be Files.

If the expression produces a value, that value must have a lifetime, and must be dropped when it reaches the end of its lifetime.

In actual Rust f = open(...) pretty clearly assigns the new file to f and it will be dropped only when f goes out of scope. The result of the assignment () is trivially dropped immediately.

Similarly in actual Rust the pointless

open(...);

without an assignment will immediately drop the temporary, closing the file.

If, in an alternate Rust, the result of f=open(...) is also the file object, then it's harder to see what should happen. It would seem that the value "falls out the left" of the expression into a temporary, but this raises two problems:

  1. Since it can't be copied, what would be left in f?

  2. When the temporary is dropped, won't the file be immediately closed?

A way around this would be to say the assignment only has a value if it's used, or only if it's a Copy type, but that seems to complicate the language to little benefit.

These reference sections touch on it but it's not very clear to me:

1 Like

I don't have any sources ready, but IIRC the reason to return () is to avoid this pervasive bug in C and similar languages:

if (errcode = -1) {
    // oops, always true!
}

In Rust, this won't compile because if requires a Boolean instead of ().

Python, as an example, solves this by making assignment a statement, i.e. banning it in expression context altogether. I assume that for Rust as an expression-oriented language, returning () was the simplest solution.

2 Likes