“Immutable variable” is an absurd term

“Variable” means “that which can vary”, i.e. the exact opposite of “immutable.” That is illogical and confused my for quite some time, when I was new. I see this at least in compiler error messages, “The Rust Programming Language” and even explicitly wrong in “The Rust Reference.”

I suggest gradually replacing these words by “immutable binding” everywhere. The word “variable” should be limited to “mutable bindings” to make sence.

Other uses of the word “variable” should be checked if they align with this. E.g. the unused_variables lint should become unused_bindings with the old name as an accepted alias, maybe deprecated in the 2027 edition.

3 Likes

"Immutable binding" still doesn't capture reality, given interior mutability.

1 Like

I don't think it's the exact opposite; that's "invariable". "Immutable" means the variable cannot be changed, which I don't think necessarily means that it cannot vary (e.g., across different executions of a function).

I can understand it confusing beginners, though.

16 Likes

I think the term "binding" is confusing in Rust context. It suggests that there is an object out there, and then separately there are bindings that bind to it. In other words, it suggests a reference.

Here is what Wikipedia says, confirming my intuition:

In programming languages, name binding is the association of entities (data and/or code) with identifiers.[1] An identifier bound to an object is said to reference that object.

It makes more sense to call Python or ML variables "bindings" because they are always references than refer to objects on the heap, and you can have multiple variables binding to the same object.

2 Likes

I don't think this premise that "variable" means "mutable" is correct at all. The term "variable" is very common throughout mathematics and logic, and in those fields, it never means anything like a mutable variable in programming languages. (Even more, mutability in that sense doesn't even really exist as a basic concept at all, really.)

Of course something should still "vary" for the name to be more sensible.. But IMO the variability of a variable doesn't need come the form of memory contents the variable refers to varying over time. Instead variables can also just vary over executions or function calls. In recursive settings, the same variable can even exist multiple times at the same time, on multiple levels of the stack. The nature of how the actual "thing" referred to by the variable [1] changes with each function call[2] is arguably the main thing of interest.


  1. in Rust, usually a place in memory; though often (if there's neither mutability nor interior mutability) you can ignore that and think of the value directly instead, which would be more similar to variables in maths ↩︎

  2. and also with each program execution, otherwise "static variable" would be a weird concept ↩︎

26 Likes

I’ve done a lot of Lisp, and always wondered in disbelief at its immutable cousin Scheme. Then Rust was my 1st language with immutability. Because of the “immutable variable” confusion, for a long time I feared I was misunderstanding something.

I get what y’all are saying. Yet often they don’t even vary over time:

let a = "I’m always the same";
let b = 27 * 1024;   // kibi, not kilo

“Binding” (a German beer) is a bit of an awkward word too. Yes, in a more active sence, one might see it as meaning reference. On a logical level it only binds a name to a value.

Interior mutability could be called a less unwieldy “transmutable,” though one might misunderstand trans what. Mutation trans an “immutable” or rather a special value transformation?

The only word that is baked into the language is “mutable” with its keyword mut. The obvious “letting” seems a bad choice, as let alone is weird too. It only makes sence together with =. And it neither catches bindings in match patterns, nor parameters.

In my picky mind, it’s still down to finding the right words. We can brainstorm to find more meaningful, easily understandable words that reconcile all these considerations. If we’re audacious enough, we could turn “a mutable” and “an immutable” into nouns, as English so readily does. That still doesn’t give a nice word for both together. I’m not loving “value name (valname)” or “value handle (valhandle)”…

"Variable that can't vary" sounds a bit silly, but I think it's fine. People understand what it's meant to say.

"Binding" is a less common name, so I don't think it would be clearer. "Binding" on macOS has an entirely different meaning (it's a run-time change observer for GUIs). Rust also uses "bindings" for FFI interfaces, and you need to squint very hard to notice how that's related.

However, I think there are much bigger problems with the feature, not in the "variable" part, but the "immutable" part:

  • The mut of let mut is similarly named, but semantically quite different from mut in &mut: let mut is just a "did you mean to change this?". You could use let mut by default on everything, and it would not affect semantics of the program. OTOH &mut has strognly enforced semantics you can't ignore.

  • It's not just about mutability (reassignment) of the binding itself. Due to mem::swap and in-place mutations of primitives it also needs to care if &mut references are taken to the value. However, that starts overlapping with other mutations that aren't swapping the value, so it makes let mut seem to care about more than just the binding, but also it's not meant to (and can't) control mutability of the value.

  • Inner &mut, interior mutability, shadowing, moves, and default mutability of temporaries can all enable values to be mutated despite existence of an "immutable" let, so let promises very little. As a user you can't interpret it naively as "this let says x is 'immutable', so I can assume later occurences of x in that scope have the same value". You only get a very narrow definition of 'immutability' that comes with language-lawyering caveats.

3 Likes

I'm not sure if I understand this correctly, but you seem to distinguish between "mutation of the binding itself (reassignment)" and "(in-place) mutations of the value".

But in Rust, unlike in Python, there is no difference! That's because in Rust variables aren't references, they contain the value directly. A reassigment is one way to mutate the value. There is no difference whether you change the value of a variable through the = operator, or through std::swap, or through a mutating "in-place" operation such as += or .increment(). All of them change the value "in-place"!

That's exactly why this "binding" terminology is, I think, super confusing in Rust and should be banished because it suggests a level of indirection that isn't there.

I agree about interior mutability (and the way it's used), that is confusing.

But if you have an immutable variable of a type without interior mutability, you have full guarantees it won't change. Copies, moves, temporaries don't change the value of the variable.

Shadowing doesn't change the value, it creates a separate value in another variable, no point talking about it as if it changes the value. The original is still there in the original variable, you may even hold a reference to it and check. So it doesn't help to think of shadowing as a modification of the value in the original variable, that's just wrong.

In the same way you can shadow a whole function with another of the same name (playground), but nobody would say you have "mutated" the function, you just have another function with the same name.

2 Likes

Yes and no. Anything that’s on the heap is mix of referenced stuff and (small) direct value. Lines 2 & 3 mutate these separately:

let mut a = String::from("I’m not always the same");
a += " as before";
a = String::new();

If one wanted to distinguish the two kinds of mutability this would be

let a = mut String::from("I’m not always the same");
a += " as before"; // possible
a = String::new(); // forbidden

For the rest, I fully agree.

Depends what you mean by semantics. It very much changes from “there is no valid reason to alter this” to “dear greenhorn intern, feel free to do whatever you want!”

This makes no sense in Rust, and thus doesn't compile, precisely because of what I said. This confirms what I said.

Of course String implementation itself uses indirection internally, but that's not what we are talking about, that's its implementation detail. If you have a string variable in Python you have two levels of indirection, in Rust you only have one.

To avoid confusing these indirection levels, it may be easier to first think of simpler non-Copy types that don't have internal indirection, such as:

// no Copy
struct Point {
    x: i32,
    y: i32,
}

What makes sence is always a question of viewpoint. It won't compile, because Rust makes this distinction only in the presence of an additional reference. But that’s a technical detail, all the more so as methods will magically dereference.

Otherwise there’d be symmetry between

let [mut] a = [mut] String::from("I’m not always the same");
let [mut] b = &[mut] a;

The 1st mut meaning I can =, the 2nd mut meaning I can call a self-mutating method on the value.

I don't understand where you disagree. The Rust code you write is obviously invalid, and for a good reason. There is no "symmetry" between the two cases: if you have mut b: &mut String there is an extra layer of indirection compared to mut a: String, and therefore, naturally there is an extra layer at which you can put the mut keyword.

If you have a &mut String, you can mutate the reference, or you can mutate the pointed-to String:

    let mut b: &mut String = ...;
    // mutate the reference
    b = &mut another_string;
    // mutate the pointed-to String via assignment
    *b = String::new();
    // mutate the pointed-to String via a method
    (*b).clear();

I mean this from a high-level perspective, how this feature presents itself to users. Its semantics depend on a particular specific definition of what is let's value, and what counts as mutation in this case. This is quite specific, and misaligned with what users may think "value" and "mutation" are. From user perspective, let name = &mut String::new() and let name = String::new() may both be "the name", and name.push_str("…") "mutate" "the name". But without getting into language-lawyer-level details, it makes let mut seem arbitrary. In both cases name.push_str() calls the same &mut self method on a String, and in the problem domain, the "name" has been "mutated" either way.

You keep assuming I have issues with `let mut`, because I just don't understand what is happening…

BTW, we've had this conversation before, so I need to repeat: I know exactly what's happening here and why. I know about temporary lifetime extension. I know the difference between a reference and a value, and that the exclusive reference is the value in one of the cases. I know let mut makes a distinction between creating a loan to its value, and the use of its value for reborrowing that doesn't take a reference to the reference-being-the-value. I know the . operator's auto deref hides the levels of indirection, and the reasons why it's like this. I know these things, in fine detail. I've been using Rust since 0.x. I've slogged through the entire Dragon Book, and have written compilers before. I know about the bindings, binding modes, values, references, places, shadowing, identifiers, lexical scoping, namespacing, copies, moves, and temporaries.

You keep assuming I have issues with let mut, because I just don't understand what is happening, and keep trying to explain Rust to me. But I know precisely what is happening, I just think that this particular combination of behaviors, minutiae of Rust's definitions, and interactions with other features combined, sucks. I'm not mistakenly assuming it behaves differently. I know it's behaving exactly as designed. I just think this design is poor, and it should behave differently.

I know it doesn't, I'm fully aware how it works, and appreciate why Rust has shadowing. I just think this combination of features makes let's guarantees borderline useless.

To me one of the important use-cases of making variables immutable is being able to assume that the thing with this identifier in this scope stays constant, but Rust doesn't have that, without many caveats [1].

In Rust seeing the same name later in the source code (within the scope of the binding) doesn't guarantee that this is this the same instance of the binding, so whatever let declared may be irrelevant. This weakens what let can help with in practice.

And even when I can establish that an identifier is for the same instance of an "immutable" binding, it's immutable only by a very shallow definition of mutation specific to let, which isn't even as strong as immutability of & references.

So to know whether something has been mutated (in loose terms) between its "immutable" let and its name appearing later in the scope, I can't rely on the let stating anything. I still need to scan all the source in between to find potential shadowing, and additionally either know the type (which thanks to type inference and auto deref may vary, and not be locally available) or carefully analyze code for mutations myself too.


  1. I'm intentionally not using Rust's terms here, because I'm not referring to what Rust has implemented, but to what would make sense to have in a programming language in general ↩︎

What you call "language-lawyer-level details" seems to me like the basics of how references work, which is at the very core of the Rust language. I don't see how somebody not understanding references can use the language effectively. They would be lost in the woods with endless trial and error.

I do agree that the automatic (de-)reference in name.push_str() is unfortunate, I would prefer this would have to be explicit. One has to learn to live with this ambiguous notation, but also one has to understand that there is implicit (de-)referencing going on here. I don't see how a programmer not understanding this would be able to write code in Rust.

I disagree with this. You say you're not discussing language rules and sound offended that I discuss them with you, but then you again say stuff like this that clearly is talking about language rules.

AFAICT it's the same exact immutability as you get through immutable & references, except that if you own the immutable variable, you additionally control the end of its life, i.e. you can drop it or move out of it, either way ending the life of the variable.

Is there anything else you can do to it? I am assuming you're not referring to shadowing here because you explicitly said so.

We're talking past each other so much. I don't know how to communicate to you that I'm trying to talk about usability and usefulness of language features, and where Rust falls short of meeting expectations. And I mean expectations in the sense "a different solution would work better for me; and this one fails in cases I care about", not in the sense "my assumptions were wrong and I'm confused".

But you keep looking at it strictly through explaining Rust's current implementation, as if it was flawless and unchangeable, and any mismatch between Rust's current behavior and user expectations could only be explained by user's ignorance and incompetence.


Assume a max-level expert user. A programmer so brilliant, they've made a fortune on Knuth's cheques. They single-handedly wrote the most detailed spec of Rust, and implemented their own Rust compiler that is 10x faster than rustc, and safer than Miri.

Such user may still not care about exact semantics of the name binding when they call name.push_str("suffix"). Not because of ignorance, confusion, misunderstanding, inability to reason about it. But from fully understanding when this doesn't make a real difference, and is not relevant to the task of appending a suffix to a name. In the problem domain of appending strings, the name get mutated either way. This makes compiler insisting on a difference between let and let mut a jarring nitpick about detail so narrow and specific only to the compiler, that it becomes disconnected from user's concern whether the name can be modified or not.

It's the like in the joke about a guy getting lost when flying a balloon. "Where am I?" shouts the guy to a villager on the ground. "You're in a balloon!". Undeniably correct, very precise, and yet useless.

let mut is always perfectly correct about what mut means for a binding, and precisely tracks the thing Rust requires it to track. But this feature is tracking specific set of behaviors related to specific kinds bindings, and that is not necessarily answering user's high-level questions whether something that is a concept in their mind gets mutated (modified) or not.

let can give you an answer like "the first instance of a binding with a symbol name, after being initialized and holding a value of type &'1 mut String, definitely does not have &mut reference taken to the value through this entire control flow graph that ends with the value being moved out of the binding".

But let can't give you an answer to "does this function change this name?"

3 Likes

Well I care.

If I don't care about the details or performance, I'd probably not want to store a reference to a String, so I'd make it an owned value:

let mut name: String = ...

If I want to store a reference and change somebody else's String, I'd make it a reference to a mutable String:

let name: &mut String = ...

Very natural. I don't understand what's jarring about it. C and C++ work the same way with pointers: you can have a const pointer to a mutable object.

But let me point out that this whole thread is a discussion about whether "immutable variable" is an absurd term -- as applied in Rust. So we're not talking about an alternative, better language with better semantics, or whether the terminology sounds reasonable to a confused beginner who somewhat misunderstands the semantics.

We're talking about whether it's accurate, with correct understanding of the semantics.

You seem hung up on shadowing rules, but that's a separate syntax-level feature, maybe it's confusing, but it's rather orthogonal to the actual semantics of immutable variables.

Focusing on the example of a super-experienced rust user, it's not that it's confusing. It's that let mut vs let seems orthogonal to the question the (highly experienced and logical) programmer is pondering.

OK: maybe making variables immutable is not useful to the programmer (I vehemently disagree -- I find it very useful), but that's not what this whole thread was about. That's a separate discussion.

The thread is about whether "immutable variables" is an accurate, reasonable description of the feature. It is (at least in cases where interior mutability isn't involved).

That seems like a good topic for a separate thread. This thread is about the name of the feature, not whether it is useful.

Focusing on perf microoptimization detail again misses the higher-level semantic absurdity of let and let mut, which are both allowing many kinds of mutation, except one particular technical difference that is so small it may not even affect program's semantics nor performance (let x = &mut X gives you an unnamed by-value storage in the same scope; or let mut x = X may look like by-value, but if you combine it with a non-move closure it gets implicitly referenced; and LLVM may optimize these things either way).

It's not just perf. The flip between different binding kinds comes up naturally during refactoring, e.g. you may start with let mut s = String::new(), but move some of the code to a helper function where it becomes &mut String. The majority of the code can stay syntactically the same, do semantically the same work, and even get inlined and result in exactly the same machine code generated, but still require different let vs let mut incantations. I fully understand why and how this is happening, but still think it's silly and feels arbitrary (and saying that it's obvious and expected, because Rust works this way is almost a circular logic - Rust cares about this specific distinction in this specific way, because that's how Rust is defined and implemented to care about this specific technicality).

I don't find this confusing. I'm only lamenting that existence of this otherwise useful feature makes guarantees given by let even less useful, because they are about one particular binding instance that may become shadowed, and it's not designed to be capable of giving guarantees about all x's in a scope.

When I read code, I may have a question like "is the value of x the same in this expression, and that expression?" (maybe because for an algorithm it's important that these two are in sync). If let has been designed to give a "freeze" kind of deep immutability and no shadowing, it'd be easy to know that x is the same in the entire scope, from the definition alone, without checking any code in between. Such "immutable variable" would be giving me a practically useful guarantee I can easily count on. But with shadowing, and possibility of x being &mut T, a let x declaration alone guarantees to me so little it's almost useless. It doesn't answer my question "is x the same here and there". It can't give me an unqualified answer to such question by definition. It gives me a different, weaker guarantee "this one instance of the x binding doesn't have &mut reference taken", but that's answering a weirdly specific question I wasn't asking. This binding technicality is not giving me useful-enough information. To know whether two instances of symbol x in a scope evaluate to the same value, I have to check if and how they're shadowed, and check whether the type of x is one of the types that can change the value of x, but let by design doesn't require mut for. So instead of just looking at the declaration, I need to check all the code in between.

3 Likes