Is it good to allow some function generate implict mut variable?

for example, If I wrote:

let vec=Vec::with_capacity(20);

In most cases, since we specify the capacity, we are going to push elements into the vec, thus, vec should be mut.

But currently, compiler complains that vec is not mut.

Is it possible to change it into an warning, I means, we could allow some function returns a "must mutable object"?

e.g.,

fn with_capacity(u:usize)->mut Self{...} // return a mutable variable.
...
fn main(){
    let x=Vec::with_capacity(20); // generate a warning
    x.push(2);// but this should compile even if we do not mark x as mut.
}

First, feature requests are off-topic here. Their place is IRLO.


Now, with that out of the way: this is not a good idea at all, for the following reasons:

  1. It displays a clear misunderstanding about mutability in Rust. Mutability is not a property of types or values. It is a property of bindings and references. Pretending as if it were something intrinsic to a type conflates two things in the type system, with no advantage.
  2. It doesn't add any value. One can already differentiate between mutable and immutable bindings; one can already get an error when an immutable binding is mutated; one can already declare variables as mutable if it is so needed.
  3. It encourages sloppiness. You say you want a "warning" and then have the code compile anyway. Decades of experience shows that nobody pays attention to optional "warnings" and "lints". It compiles, let's ship it! Rust upholds its guarantees by means of hard compiler errors – otherwise, they wouldn't really be guarantees. Relaxing this would basically degrade the compiler to a linter, and this sharply opposes the design and philosophy of the language.
6 Likes

Not necessarily. For instance, String::with_capacity could be implemented like this:

impl String {
    pub fn with_capacity(capacity: usize) -> String {
        let vec = Vec::with_capacity(capacity);
        String { vec }
    }
}
1 Like
impl String {
    pub fn with_capacity(capacity: usize) -> mut String {
        let vec = Vec::with_capacity(capacity);
        String { vec }
    }
}

This was why I put mut in the return type.


I'm REALLY SORRY since when I want a feature request, open threads in IRLO, people might send me a crate directly and told me, that thread should belong to URLO...

Actually, we have lints like #[warn(unused_must_use)], it is reasonable to told rustaceans about whether the variable should be binding as mutable variables.

it could suggest rustaceans use more appropriate code. i.e., it would alert people who create a Vec::with_capacity(1000) and never use it.

If you think #warning is not suitable, maybe #deny by default.

But IMHO, when people wrote Vec::with_capacity(100), they assume that vec should be used quickly, there is no reason to assume that they would make the vec immutable.

(In the case that allocate memory and used later, we should mark the return type as mut, thus the problem solved.)

And this wouldn't generate a warning? In your OP you said that declaring vec non-mut would generate a warning.

2 Likes

let mut is only a weak lint on the binding, rather than true immutability guarantee of the value behind the binding. There are already some "loopholes" in let mut, e.g.

fn main(){
    let x = Vec::with_capacity(20); 
    let mut x = x; // generates a new binding that shadows the old one
    x.push(2);
}

or

fn main(){
    let x = Vec::with_capacity(20); 
    {x}.push(2); // temporaries are implicitly mut
}

So personally I wish let mut requirement was downgraded to only protecting against reassignment of the binding (being explicit about single-assignment programming style is still useful), but not care about taking &mut reference out of it, but many people disagree with me.

2 Likes

This is a myth that has already been addressed several times.

1 Like

I strongly disagree, and I've also provided counter examples and explanations several times. I know you're trying to say that current semantics that Rust has chosen are robustly implemented and they do exactly what they're designed to do. I'm trying to say I disagree with the design, and the guarantee current design gives is too specific and too narrow.

"this instance of the binding x won't change" (with shadowing and moves not counting as relevant to the issue) is IMHO not as useful as a hypothetical stronger "any binding named x in this scope is guaranteed to have exactly the value as assigned at its definition" or "the value bound to x is deeply frozen for the entire duration of x's scope".

The general concept of "x is immutable" could be interpreted/implemented in multiple ways, and I don't like the way Rust has chosen to interpret it.

1 Like

Calling it a "weak lint" and saying "there are loopholes" suggests the opposite, it suggests the rules aren't 100% robustly implemented and that the compiler doesn't always catch violations.

That would defeat the whole point of shadowing. The whole point of shadowing is that the new binding is unrelated to the old binding, even though the name is same. You're basically saying "let's ban shadowing" -- a valid idea, but orthogonal to whether mut is a weak lint / has loopholes.

1 Like

The terminology around let and let mut calls them immutable and mutable, and there are examples like above where you can mutate value bound to x even though it's called an immutable binding.

You counter this with a specific language-lawyering explanation that this is fine, because Rust means it to be exactly this way, but I'm looking at it from a perspective of a Rust learner: it's a paradox: there's a let called immutable and there are non-obvious language rules that allow you mutate a thing that has just been labelled immutable.

For the same reason I don't like terminology around & being immutable and Rust having "interior mutability". This too sounds like a paradox where you can mutate immutable things.

2 Likes

The learner just has to understand shadowing properly, i.e. that your code is equivalent to:

fn main(){
    let x = Vec::with_capacity(20); 
    let mut x2 = x;
    x2.push(2);
}

There's no point trying to pretend the two x bindings are the same variable since that is a lie and would confuse people even more.

@H2CO3 put it best:

It displays a clear misunderstanding about mutability in Rust. Mutability is not a property of types or values. It is a property of bindings and references.

You calling it a "weak lint" / loopholes because the values can change after being moved elsewhere shows the same misunderstanding.

1 Like

Contrast this with what I'd call a strong(er) guarantee. If you have & without interior mutability, then you can rely on it not changing anywhere in the scope, and it this fact can't later be overriden with moves or rebinding to a "mutable" binding:

let x = &String::new();
{x}.push('N'); // not legal
let mut x2 = x;
x2.push('N'); // still can't mutate

Here I'm talking about the value originally assigned to x, not the x binding. Shadowing unfortunately is an exception to any case of reasoning about things named x.

I don't know how this stronger guarantee would work. You would have to write a proposal. The example code you wrote already doesn't compile today.

I don't mean it as a proposal, but rather as an illustration that Rust has different levels of "immutable" for & and let. With references the immutability is taken more seriously (and that's crucial for correctness) it affects entire scope of the loan, and moves or rebindings aren't bypassing this particular meaning immutability. Whereas let is also called immutable, but this meaning is narrower, and does not extend to moves and rebinding, and doesn't really affect language semantics.

In terms of proposals, backwards compatibility makes it impossible to change let to mean immutable in a stronger/broader sense. But it could be reduced to only preventing reassignment of the variable, to behave like const bindings in JS. That's not exactly an improvement, but rather a path to deprecation of what I think is a flawed feature.

Moves of the value are impossible thanks to the lifetime rules. So it's not that the reference stays immutable after a move -- there are no moves that can possibly happen during the lifetime of the reference.

Moves of the reference are a totally different thing from moving the value whose mutability we are talking about here.

1 Like

I know. When I criticize Rust, it's not from lack of understanding how it works, but from disagreeing with its design choices, or what terminology it uses to describe its actual behavior.

e.g. when someone says "5 / 2 == 2" is weird, it's not necessarily from not understanding integer types and rounding in integer division, but from the fact that the language borrows math's terminology for integers and division, but doesn't behave like the math says it should.

So my beef with immutable let is that Rust means something else than what the Oxford dictionary defines "immutable" to be.

It's the same exact meaning.

let a: String = String::new("abc");
let b: &String = &a;

a is immutable. a will not change. When the value of a is moved elsewhere it may change, but what the value behind the binding a is will not have changed.

b is an immutable reference. *b will not change. When the value behind *b is moved elsewhere it may change, but what is behind *b will not have changed.

It is not really useful to talk about objects maintaining identity when they move around. It's meaningless, like the Ship of Theseus. Suppose I do this:

let a = String::new("abc");
let b = a + "d";

Is b still the same string that a used to be? It's a pretty meaningless philosophical question. It might or might not reuse the same heap allocation, depending on how much capacity was left spare.

1 Like

I'm going to stop now. I'm trying to discuss things from a layman's perspective (emphatically how a Rust novice could reasonably expect a different behavior from Rust), but you keep teaching Rust basics to me, as if I was the layman.

1 Like

I don't understand the meaning of "but" here. If you're discussing something from a layman's perspective, it only seems appropriate that I respond to you in that same perspective.

Maybe we should consider:

fn main(){
    let v=Vec::with_capacity(200);
    let mut v=v;
    // ops, or no-op.
    let v=v;
}

This should not generate warnings, since we bind v as mutable variable in some time.

Similar things happened to your String example: we bind vec to mutable return value, thus there is no need to generate warnings there.

I agree, thus we could consider that, if any code path is mut, then the variable is mut.

What's more, since consume a variable could call its drop function, which accepts &mut self as its variable, thus, consume a variable could be regard as mut, too.

What's more, move or consume a variable is mut if and only if the variable is marked as mut in its signature.

fn signature_param(mut v:Vec<i32>){...} // signature for the parameter
fn signature_return()->mut Vec<i32>{
    let v=Vec::with_capacity(100);
    v // since return type is `mut`, v is marked as mut, thus no warning should be generated
    // call `signature_param(v)` here would consume `v`, but the way we consume `v` is `mut`, thus `v` is marked as `mut` here, no need to generate warnings.
} // signature for the parameter