Haskell has a nice feature where you can destructure a value and keep a reference to the whole value. So if f() returns a tuple (a,b,c) then q@(x,y,z) = f() will store a in x etc and also q will contain (a,b,c). This is sometimes a useful feature. Now, I don't think Rust has this feature, and I think I understand why, but I want to double check that I am getting this right.
So, Rust does not have this "as pattern" feature because destructuring moves the value, so you can't also keep a reference to the whole thing around, because it's been moved. Am I understanding that correctly?
That's the kind of trouble it's all too easy to run into when trying to mentally map a set of concepts you might have got used to from one environment/PL into another one. Haskell ships with a built-in GC. Rust does not. Destructuring a "value" in Haskell (in all likelihood) simply creates another "reference" to the (heap) pointer of the original "value". Rust does not.
"Destructuring" isn't the same, either.
In your particular example, Haskell f() isn't going to return a mere "tuple" (a,b,c) but a full-flegded BoxGC((BoxGC(a), BoxGC(b), BoxGC(c)). When you "destructure" it by q@(x,y,z) = f():
q points to the outermost BoxGC
x points to the BoxGC of a
y to the BoxGC of b
z to that of c
with all of the above being BoxGC-ed themselves, in turn.
Contrast that to Rust, where an f() returning a tuple t (which you can destructure into a (a,b,c) or (x,y,z) or (f0, f1, _) or whatever else) means that the whole tuple will be placed back on the stack (not on the heap inside any Box), and you'll figure out the rest on your own.
Big takeaway: to talk about a being "stored" in an x without any consideration for the underlying memory management model of the PL you're using is, all things considered, a fool's errand.
Even if the all fields inside of your tuple are Copy (as per @quinedot), if you "keep a reference to the whole" while you "destructure" the rest of it you'll no longer deal with the same value:
Summary
fn main() {
// `q` and `x/y/z` won't reference the same value (`i32` is `Copy`)
let ref mut q @ (x,y,z) = f_i32();
println!("before: {q:?} and ({x}, {y}, {z})");
// changing the former won't affect the latter
*q = (10, 100, 1000);
println!("after: {q:?} and ({x}, {y}, {z})");
// you can reference all the fields in a "read-only" mode
let ref q @ (ref x, ref y, ref z) = f_string();
println!("strings: {q:?} and ({x}, {y}, {z})");
// but attempting the `&` the "whole" value
// while destructuring the rest will fail
let ref mut q @ (x,y,z) = f_string();
}
This one confuses me. q is &T3<String> and x is &String, correct? But there's no variable owning the tuple, so it's referring to what has been returned from the function, which likely sits on the stack (pointing to the string content on the heap). Isn't it strange that the compiler doesn't complain about the value being dropped before it can be used?
Binding a shared reference to a temporary returned from a function triggers “temporary lifetime extension” which is pretty much exactly what it sounds like. For a lot more detail, check out Niko’s post.
I'm surprised; after hitting the "temporary value is freed at the end of this statement", I thought lifetime extension was limited to a very few cases. Good to know!
Yup, still. Unless it's optimized away at compile-time, it's not just "likely" but "guaranteed".
Nope, because it's not dropped at all. You can think of it as:
let ref q @ (ref x, ref y, ref z) = f_string();
// is (kinda sorta) the same as:
let <anon_bind> = f_string();
let ref q = <anon_bind>; // one `ref` binding `&`s the whole
let (ref x, ref y, ref z) = <anon_bind>; // the rest `&` the inner fields 1-by-1
I just meant that some types could also be in a register, but it's only an implementation / optimization detail; it can still be considered as stack material.