Why is there no `mut T` like there is `&mut T`?

First, I started with the question: Why is mut necessary for owned types? Through some reading, trying to understand that question, I found myself more confused by a seeming inconsistency/inadequacy of Rust's design choices when it comes to mut for owned types.

fn func1(mut x: String) { x.push_str("push to x") }

Why is, in this situation, the mut required on the x param?

Without it, E0596 occurs; cannot borrow x as mutable, as it is not declared as mutable. Okay, sure, that makes enough sense.

fn func2(mut y: String) { func1(y) }

Now, I go and write func2 and because I call func1, which mutates x, I assume I need to specify that y is mutable. Wrong! The compiler informs me that variable does not need to be mutable. So, I am allowed to rewrite func2 without the mut.

fn func2(y: String) { func1(y) }

No more warnings, all works.. but why? In func2, if I try to directly call the push_str(&str) method, I would have to bind y to a mutable local variable, either through the mut declaration shorthand or explicitly with let mut mut_y = y.

I did some readings in the Rust documentation, as well as some forum posts (linked below). From that, I got a better understanding of the mut x: T in the parameter of a function syntax and how the mut on the left side of the : does not affect the type the function consumes.

But it still seems to me that there is an inconsistency here: if a function is always allowed to mutate the value of a local variable by merely moving it within the same scope, why require the move to a mut variable at all? Why not just let an owned value be mutated by the owner scope? I'm having trouble seeing the benefit of the explicit use of mut if any type T can be expanded to be mutable at any time by a simple move, succinctly demonstrated but not explained as to why in Rust By Example.

With references, there seems to be more clear consistency: &T can never become &mut T. This makes a lot of sense; if the owner gave you an immutable reference, you shouldn't be able to expand your access to the value to mutate the referred value.

And with owned types T, as the owner, you do always have the ability to gain mutability with let mut mut_x: T = x, so what is the added benefit of having to explicitly do so? Why not, instead, always allow mutability of owned values? I get that it's nice to know that your value won't be mutated, but if you move it to some function, you don't know that it won't be mutated. And further, you can always just rebind it and change the value in your own scope, making it unclear from the initial declaration if the value ever changes.

// without the mut, you know that 
// x will never change (supposedly)
let x = String::from("foo");

/* some code */

// but x not changing doesn't mean the 
// value that x represents can't change
// because you can just move it to a 
// mutable variable and now it can be changed
let mut mut_x = x;

// you could even do `let mut x = x;`
// to make it extra confusing to read
// and determine what can and can't
// mutate throughout the function

What benefit do you really get from knowing that x won't be changed without knowing whether the value behind x will change? If owned types are always subject to mutating, why bother with ever requiring the mut?

Also, this could go the other direction: why can you rebind variables at any time to make them mutable? Why isn't the mutability of owned types a part of the type the way mutability of references is a part of the type. i.e. why not have let x: mut String = mut String::new() be the way mutable values are declared? And then String would be a type mismatch with mut String the same way &String is a type mismatch with &mut String.

Personally, I think this is the correct direction: you should have to be explicit about whether something will mutate. And I think the mutability being a part of the type would make sense as a way to truly provide this for owned types. As an example, Kotlin has mutability explicitly defined for both the variable and the type.

var list: List<Int> = listOf(0, 1, 2)
list.add(3) // fails, type List is immutable
list = listOf(0, 1, 2, 3) // succeeds, vars can be reassigned

val mutableList: MutableList<Int> = mutableListOf(0, 1, 2)
mutableList = mutableListOf(0, 1, 2, 3) // fails, val cannot be reassigned
mutableList.add(3) // succeeds, type MutableList is mutable

I don't think Kotlin's solution here is a perfect fit for Rust; I merely use it to illustrate how mutability could be a property of a type, while separately being a property of the binding. As far as I understand, Rust already provides this for reference types. Why not owned?


2 Likes

This is a very common misunderstanding, I had it too, but I think the actual mental model for me became a bit clearer after I realized that Rust has NO TYPE METADATA after compilation; types are replaced by memory layouts and specific instructions. When you write mut x, it doesn't 'compile' into a mutable variable; rather, it grants the compiler permission to generate 'write' instructions for that memory. Once the compiler is satisfied, the mut flag is discarded and never reaches the final binary.

This is just what I wanted to add to this conversation :'D I know it doesn't answer your questions exactly and completely, but I think it does, if you connect the other parts based on it.

4 Likes

For me, it's all about local reasoning. If I can learn everything I need about a function from it's signature, I can move faster with things like API design, code reviews, etc.

4 Likes

I get that it's nice to know that your value won't be mutated, but if you move it to some function, you don't know that it won't be mutated.

Yes, that's what handing over ownership implies. What happens to the value is now something for that "some function" to care about.

2 Likes

That too, because even in this example, func2 owns y and moves its value into func1 (another scope/transferred ownership etc), what func1 does to it is completely up to func1 :'D , at least that's how I think about it
fn func2(y: String) { func1(y) }

Yes, and I do understand that is happening.. I am wondering why that was the design choice, as in, why was it decided that there is no mutability/immutability guarantee on the type itself. For references, there is &T and &mut T, but for owned values, just T. Why not also a mut T that would allow mutations to the value, and T would mean the value is the same for the entire lifetime of the variable, regardless of its owner?

There isn't such thing of a "lifetime of the value". The lifetimes are entirely a property of borrows.

2 Likes

This is exactly what the signature tells you... except the last part. Rust's ownership model doesn't need to bother about the mutability of an owned typed once you have moved it. The previous owner shouldn't have to care if the moved value will be mutated or not, they have lost ownership of it!

2 Likes

Oh, what would be the correct term to refer to the time period for which the variable continues to have a memory allocation? As in, from the time it was instantiated to the time the final owner drops it?

And, even for references the mutability restriction is just a consequence of being allowed to keep multiple references to the same value alive.

I don't really understand your question... an owned value is... owned? You can do whatever you want to it. YOU decide whether it is mutable or not. One of Rust's main innovation is that at some point of your code the compiler can deterministically tell whether a value is immutable or not. Many of the performance optimizations directly depend on this one property O.O

1 Like

It seems that you are conflating variables and values. Variables are location that might contain a value. Moving ownership usually also implies moving the value in memory.

1 Like

Yes, and in that sense I prefer Rust't way of modelling mutability.

Kotlin's types and methods are a headache for me, literally a combinatorial explosion of headaches.

Hmm, I do see how that can make sense.. I suppose I am coming from a background in Kotlin/JVM where everything is just passed by reference everywhere and objects remain in memory for arbitrary amounts of time, so it is useful in that context to be able to guarantee immutability through the type as opposed to merely the variable.

I suppose in the Rust context, after you've moved the value, it just simply isn't relevant to the previous owner, so it can't ever really have an impact on that previous owner if there are changes made to the value moving forward.

5 Likes

Yes, it's a very different 'mental model'. Kotlin/Java are garbage collected, so you don't have to worry about how long a value stays valid or when to drop a value. But for example, in C++ a no GC language, there is actually const T, which is a source of MANY bugs :'D

1 Like

I understand that distinction. Eventually, though, the value itself will simply no longer be in memory, not just moved to a different address, but gone altogether. What term would be used to refer to the time from value creation to value dropping? Also, in this simple example, the memory address remains the same:

use std::ptr::addr_of;

fn main() {
    let x = String::from("foo"); // <-- from here
    println!("{:?}: {x}", addr_of!(x));
    
    let mut y = x;
    y.push_str("bar");
    println!("{:?}: {y}", addr_of!(y));
    
    let address = addr_of!(y);
    
    unsafe {
        println!("direct access of memory at {:?}: {}", address, *address);
    }
    
    drop(y); // <-- to here
    
    unsafe {
        println!("direct access of memory at {:?}: {}", address, *address);
    }
}

There is none in Rust documentation, that I know of. Unfortunately people often call that lifetime, which is incorrect in Rust, and there is no end to the confusion.

2 Likes

It's for code clarity and catching errors. By marking which variables are mutable, you let the compiler catch mistakes where you change a variable you didn't mean to change.

For example:

fn print_reverse(a: &[i32]) {
    let num_elements = a.len();
    let mut num_elements_remaining = num_elements;
    while num_elements_remaining != 0 {
        num_elements -= 1; // oops, I meant num_elements_remaining
        println!("{}", a[num_elements_remaining]);
    }
}

The compiler will catch this bug thanks to the fact that the variable num_elements is not mutable.

The variable y never changes, so no need to mark it mut. If you pass the string "hello" to func2, y always contains the value "hello" as long as y contains a value.

The variable x changes: it first contains the value "hello", and then it contains a different value "hellopush to x". So x needs to be marked mut.

It's not always allowed to mutate the value of a local variable. If you move a value, it's in a different variable.

Consider the first example. The value of num_elements doesn't change. I copy the value to num_elements_remaining and change that variable. Moves work the same way. If you have a value in a variable x and move it to a variable y and change that, it's the value of y that changes, not the value of x.

Note: it's variables that change, not values. Mutating a variable means that the variable gets a new value that is different from the previous value.

Let's say num_elements is 10. I set num_elements_remaining to 10 as well. Then I decrease num_elements_remaining to 9, 8, ... The value "10" never changes, 10 is always 10. The value of num_elements_remaining changes, i.e. the variable gets a different value each time it is decremented.

3 Likes

Yes, the compiler is smart and is not going to needlessly copy bytes around.

But looking at pointer values is not a very sensible thing to do, because of provenance. The compiler is allowed to assume p1 == p2 is false if the pointers were derived from different allocations, even if you can see in the debugger that they are bitwise equal.

Is this true? The docs seem to suggest the opposite:

Compare arbitrary pointers by address. Pointer comparison ignores provenance and addresses are just integers, so there is always a coherent answer, even if the pointers are dangling or from different provenances.