Why is it possible to modify "self" without making it mut

In the following code from rustlngs, we are able to change self without making self as mutable. I wonder why this is even allowed by Rust compiler:

trait AppendBar {
    fn append_bar(self) -> Self;
}

impl AppendBar for String {
    // compiler allow us to change the value of self, even the parameter is not "mut self"
    fn append_bar(self) -> Self {
        self + "Bar"
    }
}

fn main() {
    let s = String::from("Foo");
     let s = s.append_bar();
    println!("s: {s}");
    
}

You are not modifying self there. You are adding self and "Bar" and then returning that new value.

The self passed in is passed by value so append_bar takes ownership and that self is dropped when append_bar returns.

3 Likes

Does this make it less surprising?

//                           vvv
fn add_from_strings_add_impl(mut this: String, other: &str) -> String {
    this.push_str(other);
    this
}

impl AppendBar for String {
    fn append_bar(self) -> Self {
        add_from_strings_add_impl(self, "Bar")
    }
}

(Here's the actual implementation.)


Without mut on a binding, you can't overwrite the variable (once initialized) and you can't take a &mut _ to the variable. That's pretty much it. You can still move it -- including moving it to a new binding which does have mut. That's what's going on here (over a function boundary in this case, but you could inline the operation too).


Incidentally Rust also has shared mutability and you can also mutate through a x: &mut T without putting mut on x itself.

1 Like

If we consider this code as valid. The following code must be valid as well. Since String and Vec are both on heap not on heap. Hence the behaviour of both of them must be the same:

trait AppendBar {
    fn append_bar(self) -> Self;
}

impl AppendBar for Vec<String> {
    fn append_bar(self) -> Self {
        self.push("Bar".to_string());
        self
    }
}

Calling push requires a &mut _ to self, but that's not allowed without mut self, so you get an error.

One can argue about how sensible mut bindings are or are not, but that's how they work.

Whether data is on the heap or not doesn't effect how mut bindings work.

7 Likes

Let's simplify the example by removing traits and Strings and just deal with a regular function operating on i32:

fn next_number_a(x: i32) -> i32 {
    x + 1
}

fn next_number_b(mut x: i32) -> i32 {
    x += 1;
    x
}

In next_number_a: x never changes, so it doesn't need to be marked mut.

In next_number_b: x changes, so it needs to be marked mut.

It's the same reason that mut is required in one of your implementations and not the other.

Note that mut on a parameter does not affect function signature. It doesn't matter to callers, it is an internal implementation detail of the function.

9 Likes

I think the confusion come from the return type of -> Self. Which is really equals to only -> String in that case. You can try to replace it.