Understanding struct-field mutation
Asked Answered
C

4

18

From the Rust book about how to mutate struct fields:

let mut point = Point { x: 0, y: 0 };
point.x = 5;

and later:

Mutability is a property of the binding, not of the structure itself.

This seems counter-intuitive to me because point.x = 5 doesn't look like I'm rebinding the variable point. Is there a way to explain this so it's more intuitive?

The only way I can wrap my head around this is to "imagine" that I'm rebinding point to a copy of the original Point with a different x value (not even sure that's accurate).

Chatman answered 31/7, 2015 at 17:11 Comment(0)
C
8

I had the same confusion. For me it came from two separate misunderstandings. First, I came from a language where variables (aka bindings) were implicitly references to values. In that language it was important to distinguish between mutating the reference, and mutating the value that was referred to. Second, I thought by "the structure itself" the book was referring to the instantiated value, but by "the structure" it means the specification/declaration, not a particular value of that type.

Variables in Rust are different. From the reference:

A variable is a component of a stack frame...

A local variable (or stack-local allocation) holds a value directly, allocated within the stack's memory. The value is a part of the stack frame.

So a variable is a component of a stack frame -- a chunk of memory -- that directly holds the value. There is no reference to distinguish from the value itself, no reference to mutate. The variable and the value are the same hunk of memory.

A consequence is that rebinding a variable in the sense of changing it to refer to a different hunk of memory is not compatible with Rust's memory model. (n.b. let x = 1; let x = 2; creates two variables.)

So the book is pointing out that mutability is declared at the "per hunk of memory" level rather than as part of the definition of a struct.

The only way I can wrap my head around this is to "imagine" that I'm rebinding point to a copy of the original Point with a different x value (not even sure that's accurate)

Instead imagine you are changing one of the 0's in a hunk of memory to a 5; and that that value resides within the memory designated by point. Interpret "the binding is mutable" to mean that you can mutate the hunk of memory designated by the binding, including mutating just part of it, e.g. by setting a struct field. Think of rebinding Rust variables in the way that you describe as not expressible within Rust.

Combes answered 1/8, 2015 at 19:4 Comment(2)
You set me on the right track. I needed to write a demo to see it in action (see my answer).Chatman
@Chatman Thanks for posting your demo. That's a nice way of putting it.Combes
M
13

This seems counter-intuitive to me because point.x = 5 doesn't look like I'm rebinding the variable point. Is there a way to explain this so it's more intuitive?

All this is saying is that whether or not something is mutable is determined by the let- statement (the binding) of the variable, as opposed to being a property of the type or any specific field.

In the example, point and its fields are mutable because point is introduced in a let mut statement (as opposed to a simple let statement) and not because of some property of the Point type in general.

As a contrast, to show why this is interesting: in other languages, like OCaml, you can mark certain fields mutable in the definition of the type:

type point =
   { x: int;
     mutable y: int;
   };

This means that you can mutate the y field of every point value, but you can never mutate x.

Meier answered 31/7, 2015 at 17:26 Comment(0)
H
8

Here "binding" is not a verb, it is a noun. You can say that in Rust bindings are synonymous to variables. Therefore, you can read that passage like

Mutability is a property of the variable, not of the structure itself.

Now, I guess, it should be clear - you mark the variable as mutable and so you can modify its contents.

Helotism answered 31/7, 2015 at 17:25 Comment(2)
Ok, I think that helps a bit. So it's as though mut in front of a variable just applies a mutable property to the variable. And that property implies certain things, e.g. rebinding, and struct-field modification. Is that accurate?Chatman
Yes, you're mostly right (except that mut does not affect rebinding in a sense that you can still write let x = 10; let x = 12 even if x is not mut. What mut actually allows is assignment into the value (either the variable itself or a field inside it if it is a structure) and taking &mut references, again, to the value or to a subfield.Helotism
C
8

I had the same confusion. For me it came from two separate misunderstandings. First, I came from a language where variables (aka bindings) were implicitly references to values. In that language it was important to distinguish between mutating the reference, and mutating the value that was referred to. Second, I thought by "the structure itself" the book was referring to the instantiated value, but by "the structure" it means the specification/declaration, not a particular value of that type.

Variables in Rust are different. From the reference:

A variable is a component of a stack frame...

A local variable (or stack-local allocation) holds a value directly, allocated within the stack's memory. The value is a part of the stack frame.

So a variable is a component of a stack frame -- a chunk of memory -- that directly holds the value. There is no reference to distinguish from the value itself, no reference to mutate. The variable and the value are the same hunk of memory.

A consequence is that rebinding a variable in the sense of changing it to refer to a different hunk of memory is not compatible with Rust's memory model. (n.b. let x = 1; let x = 2; creates two variables.)

So the book is pointing out that mutability is declared at the "per hunk of memory" level rather than as part of the definition of a struct.

The only way I can wrap my head around this is to "imagine" that I'm rebinding point to a copy of the original Point with a different x value (not even sure that's accurate)

Instead imagine you are changing one of the 0's in a hunk of memory to a 5; and that that value resides within the memory designated by point. Interpret "the binding is mutable" to mean that you can mutate the hunk of memory designated by the binding, including mutating just part of it, e.g. by setting a struct field. Think of rebinding Rust variables in the way that you describe as not expressible within Rust.

Combes answered 1/8, 2015 at 19:4 Comment(2)
You set me on the right track. I needed to write a demo to see it in action (see my answer).Chatman
@Chatman Thanks for posting your demo. That's a nice way of putting it.Combes
C
7

@m-n's answer set me on the right track. It's all about stack addresses! Here's a demonstration that solidified in my mind what's actually happening.

struct Point {
    x: i64,
    y: i64,
}

fn main() {
    {
        println!("== clobber binding");
        let a = 1;
        println!("val={} | addr={:p}", a, &a);
        // This is completely new variable, with a different stack address
        let a = 2;
        println!("val={} | addr={:p}", a, &a);
    }
    {
        println!("== reassign");
        let mut b = 1;
        println!("val={} | addr={:p}", b, &b);
        // uses same stack address
        b = 2;
        println!("val={} | addr={:p}", b, &b);
    }
    {
        println!("== Struct: clobber binding");
        let p1 = Point{ x: 1, y: 2 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        let p1 = Point{ x: 3, y: 4 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);
    }
    {
        println!("== Struct: reassign");
        let mut p1 = Point{ x: 1, y: 2 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        // each of these use the same addresses; no new addresses
        println!("   (entire struct)");
        p1 = Point{ x: 3, y: 4 };
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);

        println!("   (individual members)");
        p1.x = 5; p1.y = 6;
        println!(
          "xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
          p1.x, p1.y,            &p1,            &p1.x,      &p1.y);
    }
}

Output (addresses are obviously slightly different per run):

== clobber binding
val=1 | addr=0x7fff6112863c
val=2 | addr=0x7fff6112858c
== reassign
val=1 | addr=0x7fff6112847c
val=2 | addr=0x7fff6112847c
== Struct: clobber binding
xval,yval=(1, 2) | pointaddr=0x7fff611282b8, xaddr=0x7fff611282b8, yaddr=0x7fff611282c0
xval,yval=(3, 4) | pointaddr=0x7fff61128178, xaddr=0x7fff61128178, yaddr=0x7fff61128180
== Struct: reassign
xval,yval=(1, 2) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
   (entire struct)
xval,yval=(3, 4) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
   (individual members)
xval,yval=(5, 6) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0

The key points are these:

  • Use let to "clobber" an existing binding (new stack address). This happens even if the variable was declared mut, so be careful.
  • Use mut to reuse the existing stack address, but don't use let when reassigning.

This test reveals a couple of interesting things:

  • If you reassign an entire mutable struct, it's equivalent to assigning each member individually.
  • The address of the variable holding the struct is the same as the address of the first member. I guess this makes sense if you're coming from a C/C++ background.
Chatman answered 24/8, 2015 at 16:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.