Why isn't mutability always inherited through bindings?


#1

Hey everyone - Very new to Rust so sorry in advance if I missed something basic.

So I know that in Rust, if you are given an immutable reference to something, that immutability is transitive - You can’t modify the fields of a struct, nor any other struct through mutable references in the first one.

However, I noticed that the same logic doesn’t seem to apply for bindings, and at-first-glance seems a bit inconsistent.

With immutable bindings, you can’t modify fields of your struct or other structs you contain (own); however, you can modify other structs you hold mutable references to.

Code example:

struct Bar {
    val: i32,
}

struct Foo<'a> {
    val: i32, // A value field
    bar: Bar, // An owned field
    x_ref: &'a mut i32, // A mutable reference field
}

fn main() {
    let mut x = 5;

    {
        let foo = Foo { 
            val: 6, 
            bar: Bar { val: 15 },
            x_ref: &mut x
        };

        // This is illegal because binding is immutable
        // foo.val = 7;

        // Also illegal to mutate child structures
        // foo.bar.val = 20;

        // Also illegal - Immutability of references is transitive
        // *(&foo).x_ref = 10;

        // This is fine though... Why?
        // Immutability of binding isn't transitive to references
        *foo.x_ref = 10;
    }

    println!("{}", x); // Prints '10'
}

Is there a reason why this was done this way? Maybe I just don’t understand the difference between binding vs reference properly?

Thanks for any help!


#2

Immutability of foo means that you can not change its data, and in this case data in x_ref is just a number, which interpreted by compiler as a mutable reference. So you can’t change this number by writing foo.x_ref = &mut some_number;, but you can use it to write data to the region of memory at which this number points to.

Same goes for function signatures:

// you can change data behind the pointer
fn foo(x: &mut u32) { .. }

// you can change data behind the pointer and pointer itself
fn foo(mut x: &mut u32) {
    let mut a = 1u32;
    // e.g. like this, now pointer points to `a`
    x = &mut a;
}

#3

But doesn’t that same logic also apply equally to references?

If you hand me an immutable reference to a struct, I’m not modifying any of the bits if I just use it to read one of its fields containing a number that happens to be a mutable reference.

So it seems like there are 2 different behaviors here: Mutability is transitive when it’s through a reference, but it isn’t when it’s through a binding is only transitive to owned fields through a binding.


#4

Well, my explanation was for owned data, it becomes slightly more complex when you borrow it, e.g. here we’ll create two borrows which have the same pointer in x_ref:

let borrow1 = &foo;
let borrow2 = &foo;
// this does not compile
*(borrow1.x_ref) = 10;
*(borrow2.x_ref) = 15;

Rust disallows to mutate data behind immutably borrowed mutable pointer (essentially& &mut T), because other immutable borrows can exist, which will break Rust aliasing rules.


#5

Let’s take it these assignments out of context and put them into functions:

fn takes_foo_by_ref(foo: &Foo, i: &i32) {
    *foo.x_ref = *i;
}

To allow this would be unsound. Anybody could have a reference to the same Foo, and therefore, anyone could have a reference to the data pointed to by x_ref, leading to aliasing. For all we know, i points to the same data as x_ref:

let mut x = 5;
let foo = Foo { x_ref: &mut x };
let foo_ref = &foo; // this is totes allowed
let x_ref: &i32 = &*foo.x_ref; // this is totes allowed
takes_foo_by_ref(foo_ref, x_ref); // and therefore this is totes allowed

fn takes_foo_by_value(foo: Foo, i: &i32) {
    *foo.x_ref = *i;
}

This is clearly safe; nobody else can possibly have any references into x_ref’s referent, because we own a &mut reference to it. Should it be allowed? idunno.


#6

^ Indeed. That’s what I’m wondering as well.

Thanks for the detailed replies :slight_smile: It’s definitely helping me understand the difference more.

I see now why it’s technically “safe” to mutate a reference contained in an immutable binding, but is it semantically “correct”?

Because technically, it’s always “safe” for me to mutate through any binding with no borrows. So why even bother with the entire “immutable binding” concept then? It seems like the main purpose of “immutable binding” is to prevent the programmer from accidentally mutating a value they didn’t mean to, even though it would be perfectly safe for them to do so.

But doesn’t it seem like if you’re going to say “this binding is immutable”, the expectation is that you can’t change anything about the bound value or anything it references, just like an immutable reference?


#7

So, I guess what it boils down to is that you can mutate through a &mut member in the same way you can mutate through the &mut itself:

// You don't need to write `mut p` here to be able to
// mutate through `p`
let p = &mut x;

Intellij Rust has a neat feature that applies special highlighting to mutable variables. Notably, it highlights not just mutable bindings, but also immutable bindings with &mut type.


This is correct; unlike mutable references, mutable bindings are really nothing more than a statically-enforced documentation of intent. And it isn’t 100%. But that won’t stop us Rustaceans from loving it. Just look at how this internals thread exploded.


#8

Thanks for the link man! So it looks like my observation here is already fairly well-known to the Rust community.

It looks like what the one guy in that thread is proposing for “freeze” is more-or-less what I expected from immutable bindings - that they would enforce immutability the same way an immutable reference does.

I also like your quote from the other thread:

This is the impression I got too. Seems a bit unfortunate for a language that seems so well-designed otherwise (to me, coming from C++).

I wonder if it would’ve made more sense for “bindings” in Rust to be strictly about ownership, and for all operations - addition, subtraction, field access, etc to all be done through references. Seems like it would be more consistent - All access is subject to reference rules, and bindings are simply about having a name and owner for a value.


#9

My feelings are mixed. What prompted me to say that was how people were talking about mutability like it was part of the type system. But I like @albel727’s response to my claim as well:

I cannot deny the fact that, regardless of whether mut annotations are perfect or not, they still improve the readibility of the vast majority of code.