Mut/non-mut on name bindings useless?

I have just leart today that this code in fact compiles

let a = "hello".to_string();
let mut b = a;

A move from non-mut to mut variable!
So mut/non-mut on name bindings can be scrapped? Since they are totally useless?
The language would have been just as useable and expressive without them?
But a bit easier to understand?

On the contrary, this is extremely useful. You can only change mutability when you are moving ownership, which means there cannot be any borrows, and thus, no other code can observe that it will be modified.

Picture if this were happening behind a function call:

fn main() {
    // we have no need to modify 'v' here, so we can make it non-mut
    let v = vec![2, 6, 1, 6, 3];

    let v2 = sorted(v);
    println("{:?}", v2);
}

// sorted could be written this way...
fn sorted<T: Clone + Ord>(vec: Vec<T>) -> Vec<T> {
    let mut clone = vec.clone();
    clone.sort();
    clone
}

// or it could be written this way, without the unnecessary allocation.
//
// The code in main cannot observe any possible difference,
//  so why should we forbid main() from using it?
fn sorted<T: Ord>(mut vec: Vec<T>) -> Vec<T> {
    clone.sort();
    clone
}
2 Likes

Indeed, non-mut bindings to data you own are semi-usefull, since, as you found out, you can "just" rebind mutably. It is however only possible because a owns the data (the string), so it's free to have the data moved out into a mutable binding.

It's however very usefull when talking about data you don't own, but only are handed a reference to. Because in that case, you can't do what you do in your example.

Well there mut/non-mut is part of the type since you can have all 4 of

let r1 : & i32 = ..
let mut r2 : & i32 = ..
let r3 : & mut i32 = ..
let mut r4  : & mut i32 = ..

So I called distinction between r1 and r2 useless, while you called distinction between r1 and r3 useful. We essentially never contradicted each other

Yes I did not contradict you, just elaborate. I just said it's "semi-usefull" because rebinding mutably is not something you do accidentally, so declaring variables non-mutable means the compiler will remind you of that if you accidentally mutate it. So I'd not say it's "useless", even though I'm not contradicting you :slight_smile:

1 Like

Well.. but.. we already can not move out from variables that have been borrowed, right?
So just the same we could have been prevented from modifying variables that have been borrowed?

Wrt to @ExHP's code example, note the following works:

fn main() {
    let v = vec![2, 6, 1, 6, 3];

    let v2 = sorted(v);
    println!("{:?}", v2);
}

fn sorted<T: Ord>(vec: Vec<T>) -> Vec<T> {
    let mut v = vec;
    v.sort();
    v
}

Not surprising, still the same "moving ownership" thing.

Just now I'm trying to imagine how Rust would be if "rebinding mutably" was forbidden. Sounds like an additional safety net I'd like to have, or are there applications I don't see?

Well… but… we already can not move out from variables that have been borrowed, right?
So just the same we could have been prevented from modifying variables that have been borrowed?

I'm not sure where you are going with this. Can you show an example?


I should also point out that default immutability is just a convenience to the reader of code. Somebody looking at the code can easily tell what state is capable of changing between e.g. iterations of a loop because in general very few things are mut. (actually, this bit is muddied by threadsafe types which use &self everywhere, since the semantics of &mut are really moreso like "this pointer has unique access").

But certainly, you could make every single name binding in rust mut and it would continue to compile all the same. Except statics.

1 Like

I'm pointing out we already can not move out from variables that have been borrowed
We already can't modify mut variables that have been borrowed either. This does not compile:

    let mut a = "hello".to_string();
    let b = & mut a;
    a = "moo".to_string();

Yes it is. And we've established it's indeed the only purpose it serves.
All things borrowing-related are covered for mut variables just fine.
So for instance mut is useless on public function declaration on an API
(Not sure if mut is even part of function signature.. hmm..)

Same as @KillTheMule I was actually wondering what would it have meant to the language if moves from non-mut to mut variables were disallowed...

BTW I've removed my last two posts on internals thread since they are not contributing much. And thanks a lot for clearing my mind on this subject.

It gets stripped by rustdoc.

(this is where I would normally link to an example in the wild, but I only just tested on my local codebase with cargo doc :f)

An interesting blog post way back from 2014 about this issue: Baby Steps

4 Likes

Describes my struggles learning Rust exactly :clap: :clap: :clap:

To be honest, I'm glad you made this thread. Even though I knew at some level that immutable bindings were not required in any way for rust's safety guarantees, I never actually really thought about it.

Re: the 2014 blog post- I've since come more around to its point, and it's kind of moot now after 1.0, but the community really didn't like the idea at the time (the whole thing was called the "mutpocalypse").

It's important to know that mut on locals isn't needed for safety, but at the same time people do seem to appreciate the default of immutability and the speed bump of mut. It's also important to realize how fundamental Cell is, because thinking of &T as "immutable" rather than "shared" makes it harder to come up with, and realize the full potential of, things like pointer graphs within arenas; at the same time I'm not sure we ever had a good replacement syntax for &mut that would have conveyed that intuition anyway.

We can definitely teach and document this better. I'd love to see more people suggest the arena solution to graphs rather than just the index-based solution. I'd also like to see Cell become more ergonomic, for example by allowing more conversions like &Cell<[T]> -> &[Cell<T>] where they're safe. As much as the OO-style sea of GCed objects is messy and perhaps bad design, it could certainly be made much easier without compromising memory safety or even the explicit opt-ins to incrementing refcounts or pessimizing references in the optimizer.

1 Like

Yeah! I definitely think that we should teach "&mut is really &unique" early on!

Arena-based graphs are nice, but a bit esoteric: you can't make an owned version of it without solving self-referential structs, and index-based solutions have some nice properties (u32 for indices, serializable to JSON, struct of arrays layout).

However, understanding &unique is important for day-to-day programming as well. Recently, I was adding a cache to the thing I am writing. If I had been thinking in terms of immutable/mutable, I would have started with using &mut Cache everywhere, because, well, a cache is a HashMap, and I need to mutate it. Thinking in terms of unique/shared gives a completely different result: this is a cache, so it must be shared across different computations (potentially running in different threads), so I must use & Cache, and use some form of interior mutability.

Is there a more modern blog post on "the biggest lie of Rust"?

1 Like

I'm not aware of one! Manishearth's post The Problem With Single-threaded Shared Mutability is related since it gives some great examples of when interior mutability isn't safe, though it stops short of describing when it is.

The other place I've seen some good discussion of this is the discussion around RFC 1789, as_cell, and particularly glaebhoerl's comment on the boundaries of what Cell could do safely.

That's all tangential but also, I think, important to any explanation of why &T doesn't mean "immutable" but rather "shared."

1 Like

On the question of arena-based vs index-based graphs, I think it would be great for beginners to Rust to see a real implementation of the arena-based style. Currently, "how do I convert my sea of GCed objects to Rust?" is generally answered in two ways. One way is "don't do that," which is a pain to hear no matter how nice index-based graphs are in the end. The other is "use Rc<RefCell<T>>," which is a pain to use and feels more like workaround than a solution.

So even if they don't end up using an arena-based graph, I think it would help to know a) that it's possible, and more importantly b) what its limitations are, since it deals more with the true boundary of what's allowed, rather than the confusion of &T-as-immutable. It replaces the GC they're used to with a relatively straightforward use of lifetimes. It retains the mutability they're used to and delineates when it would be unsafe.

It may be too esoteric for some, but it's definitely a good alternative to have on the table alongside index-based and Rc<RefCell<T>>.

1 Like

Wow, I've been programming in Rust for quite a while and I didn't realize that you can go from &mut [T] to &[Cell<T>]. That's illuminating, thanks a lot!

TLDR of the RFC 1789: one can do

extern crate alias;
use std::cell::Cell;

fn pairwise_diffs(xs: &mut [i32]) {
    let xs: &[Cell<i32>] = alias::slice(xs);
    
    // Iterating & mutating *without* indices!
    for (x, y) in xs.iter().zip(xs[1..].iter()) {
        x.set(y.get() - x.get())
    }

}

fn main() {
    let mut xs = [1, 1, 2, 3, 5, 8, 13, 21, 34];
    pairwise_diffs(&mut xs);
    eprintln!("xs = {:?}", xs);
//  xs = [0, 1, 1, 2, 3, 5, 8, 13, 34]
}

EDIT: too bad I can give only one like to a comment, so here's a hand crafted one for you: :heart:!

1 Like

Yes.

Debatable!

When i asked the same question on r/rust, the response was that mut on an owning variable is useful documentation. It doesn't really affect safety, because you can get rid of it so easily, but it makes it clear what you intend to do with the variable, and acts as a small speedbump if someone tries to do something different. If i had been in charge, i would have left it out, but luckily, i wasn't.