In Rust, a binding/variable is always an independent place, a separate piece of memory â it is never possible to have both a and b be different names for the same place. If you write
let x = 1;
let a @ b = x;
then you are making two copies of x, and if you write
let x = 1;
let ref a @ ref b = x;
then you are making two separate references that both point to x.
So, in your code, with no initialization, all you are doing is creating four uninitialized places. There's nothing that connects them together after they are created. The only reason this code is allowed is that it would take work to disallow it.
How does that change when declaring uninitialized references on their own?
Because this:
let a @ b @ ref mut c @ ref mut d @ _;
d = &mut true;
fails with:
error: cannot borrow value as mutable more than once at a time
--> src/main.rs:2:17
|
2 | let a @ b @ ref mut c @ ref mut d @ _;
| ^^^^^^^^^ --------- value is mutably borrowed by `d` here
| |
| value is mutably borrowed by `c` here
Do both c and d in this example bind to the same (uninit) value, underneath?
I would bet that this case is more of an accident than any intentional part of Rust's design. There's no particular reason to write code like this, so no effort has gone in to making it behave in a flexible (or even meaningful) way.
There are still some inconsistencies IMO. There is some borrow check on the pattern which can rule a pattern... let's call it problematic...,[1] which
is based on the concept of the pattern applying to a single place
considers the type of the entire pattern to distinguish moves from copies
but doesn't consider all possible errors of applying to a single place, just some borrow-checker relevant subset
Thus if it borrow checks and there is immediate initialization, there can still be further type-based errors (despite not being problematic).
In contrast, if it checks and there is no immediate initialization, the bindings act like independent identifier bindings tied together by the overall type -- but no longer tied to a single place. So the borrow check could conceivably consider less patterns problematic if there was no initialization.
For example, the "borrowed after moved" check considers the type involved.
// This compiles (so it is not problematic)
let a @ ref b: i32;
// But this is problematic
let a @ ref b: String;
But "use of a moved value" is not part of the check:
// This compiles (so it is not problematic)
let a @ b: String;
// But this fails
let a @ b = String::new();
// (And this is fine all around)
let a @ b = 0;
And to demonstrate the independence of place for uninitialized bindings that pass borrow check:
// This compiles (so it is not problematic)
let ref a @ ref b;
// And `*a` and `*b` need not be the same place
a = &'a';
b = &'b';
println!("{a} {b}");
Yet as explored above,
// This is problematic despite having no initializing expression
let ref mut c @ ref mut d: char;
So altogether I agree with this vibe:
given the examples, "invalid" feels a bit off âŠī¸
That's what confused me: the book, if I remember it well, only ever mentioned the @ as a way to bind whole variants in a match opt { var @ Some(_) => var, _ => todo!() } kinds of expressions. Discovering that it could be used in any regular let statement was ... intriguing.
I wouldn't imagine anyone, while being in a serene and quiet state of morbid sanity, would even attempt it to begin with (happened to stumbled upon it while toying around with an idea of a Rust's interpreter, myself). The let itself merely matching against "any irrefutable pattern" never occurred to me, even as an idea, however. Declaring a placeholder variable for the value, yet to be placed on the stack? Certainly. Destructing an existing enum or a struct? Sure. But let a @ b @ ref c;?
Irrefutable pattern as an input to a let binding, as a language design choice? Brilliant.
It would be fun to take a look at a fully functional piece of Rust code that would consist (almost) exclusively of the lesser-known, never-used, jarring yet perfectly-valid pieces of syntax that would make no sense to the reader - and all the sense in the world to the compiler, now that I think of it.
let (): () = (); is another promising candidate. Having () as a value, a type, and a pattern, all at once, is just ... brilliant. Yet horrible. Yet brilliant, mostly. Yet still, what in the ...
P.S. Make it let (): () = match () { () => () } as ().
I've used let () = some_func()?; on occasion, to be crystal clear that nothing important is being ignored. Fully specified values as let patterns can also be useful for things like
let true = some_condition else { return Err(..).into() };
(or more generally, let patterns with no bindings).
How is this different from let _ = some_func()?;, if at all?
I can see it being used (and I've definitely done it myself) to match against enum variants without any #[derive(PartialEq, Eq) for ==, but plain let true = condition else { diverge!() }?