How should we talk about mutability?

So, in the new TOC, I had http://doc.rust-lang.org/nightly/book/mutability.html set up to talk about mutability in Rust. @alexcrichton and I were discussing this earlier, and given interior vs exterior mutability, saying that, for example, a &T is immutable is wrong in a strict sense. Which leads to things like this: https://github.com/steveklabnik/rust/commit/6476a1e37844f6d1cd63914df616ae577fa6fc64

How should we go about talking about things like this? Clearly communicating these concepts is a very important part of Rust, and how people will see the language when they come check it out at 1.0.

Thoughts?

1 Like

I think that saying that they are immutable is Ok, but maybe have a dagger(ǂ) and a note with a footnote about interior mutability through the Cell family of objects, a la

ǂ Rust actually contains Inner mutability through the Cell family of objects, but that is a topic for a later chapter.

Not a huge fan of forward references, but if you want to be completely correct, then that might be the best option.

1 Like

I think it’s completely acceptable to tell a white lie here and say that &T is immutable, rather than burdening early users with the Cell types. Note that saying there is no support on the language level is still a bit of a lie, since UnsafeCell must be provided by the language.

3 Likes

I like this in theory, but then, every time we talk about mutability, we need the daggar, so it seems redundant.

This question is a bit broader than just the book itself, I mean in general, across everything.

It might be worth looking at the terms D uses (the whole page is probably worth reading, but I linked directly to the first of the four more relevant sections): http://dlang.org/const-faq.html#transitive-const

I’m pretty happy talking about immutable things and meaning pretty-much-immutable. As long as interior mutability is covered early and talked about often, then the convenience outweighs the risk of confusion.

(It’s worth pointing out that mut really means unique though, and mutability is a side-effect of that. If we’re talking about the properties the type system enforces then starting with uniqueness, ownership, and borrowing is better than talking about mutability. Safe mutability is the consequence (though often what the programmer is actually interested in)).

4 Likes

I think it’s fine to call & a shared reference and note that for the large majority of types, accessing them through a shared reference means that they will be immutable.

3 Likes

I’m okay with just “immutable” for &Cell<T> and friends. The concept of interior mutability is something which comes from functional programming AFAICT and the meaning should be clear to people who already know about interior mutability. Those who don’t will probably equate “immutable” to shallow immutability. Whenever I’ve explained this people have always assumed shallow immutability.

Calling it immutable with a footnote is the best option I guess.

I agree. I also don’t expect anybody to assume that &T really means it is immutable. Unless it is pointing to some read-only memory, it cannot be true as you could always just write to the pointer address ignoring the restrictions imposed by the language…

Maybe talk about sufficiency for mutability. Like “holding a shared reference to a value is not sufficient to provide mutable access to it, since a shared reference cannot guarantee that the access it provides to a value is unique” and “mutable references provide unique—and thus mutable—access to a value provided that the mutable reference itself is accessed uniquely (i.e. not through a shared reference)”. As for fields, maybe it should say “Rust does not support applying mut to the individual fields of a struct”.

Another possible approach is to talk about the assignability of fields rather than their mutability, which doesn’t engage mutation as an effect of calling a method and/or operating on the result of a method call. Then you can say you can only make mutable references to struct fields that are assignable.

1 Like

From my perspective as a C++ programmer talking about “interior mutability” is a bit nebulous. I had to look up the documentation for Cell and RefCell not that long ago to figure out what it was, and my curiosity was piqued by discussion and code containing usage of Cell/RefCell, not any mention of internal mutablity. I think it would be a mistake to assume this term is in common usage in the programming community.

I think from a C++ (and non-functional) perspective it is important to distinguish external mutability as being managed at compile-time (using Rust’s borrow system) while internal mutability is a runtime construct, at least in the context of the current Rust implementation. Definitely talk about external mutability first, emphasizing the fact that it is managed and enforced at compile time. From there add internal mutability as an extension to external mutability, emphasizing the dependence on runtime management and checking. People tend to liken Rust’s borrow checker to std::unique_ptr or std::shared_ptr; I think this is probably a reasonable parallel for unique_ptr (a scoped compile-time construct), but shared_ptr is much closer to Cell<T> and internal mutability in terms of how it is implemented.

Maybe instead of saying &T is immutable in a strict sense, emphasize that it is immutable at compile time, and sort of leave the runtime distinction as an ambiguous dark cloud until you get around to explaining Cell<T>/internal mutability. Possibly even have a small section which talks about internal mutability just after external mutability without explaining the how or why until later.

I think you shouldn’t wait too long before introducing internal mutability; it seems to feature to some degree in many Rust libraries I’ve seen. It certainly needs to be introduced sooner than it is currently.

4 Likes

I vote for Cell types to be covered sooner than later, preferably in this manner (“when to choose interior mutability” was very helpful to me). I admit that, as a C++ programmer, I had never heard of “interior mutability” before Rust. In fact, Googling the term brings back nothing but Rust-related results. The concept is familiar to anyone who in C++ has reluctantly removed the const qualifier from a function because it has an internal side-effect. Still, I would guess that the solution to this problem (internal/interior mutability) is not well known by many.

With such strictly enforced mutability rules, the Cell types are strictly important. Perhaps it’s best to introduce Cells around the same time smart pointers are introduced.

Off the top of my head, just-woke-up attempt:

A &T is an immutable borrow; that is, having one does not allow you to mutate the thing being pointed to. Since Rust prohibits having both a &mut T and any &T at the same time, this means that if you can't mutate a given pointee, neither can anyone else.

There is an escape hatch in the form of "interior mutability" types Cell and RefCell, but those will be discussed later.

In other words, I'd try to phrase it along the lines of "immutable" not providing mutability, as opposed to banning it. When you introduce the cell types, you can then expound that the actually important part of all this is that there is no simultaneous mutable/non-mutable access to a value: &T/&mut T check this at compile-time, the cell types allow an escape hatch that's checked at runtime.

1 Like

Maybe we should treat this the same way we treat unsafe? Its the same idea, ‘we promise this’ -> ‘but there is an escape hatch’

I generally have found thinking about interior mutability to be easier when thinking of it in terms of a flock style lock; where either one process can obtain an exclusive lock, or any number can obtain a shared lock. The exclusive locks are generally used for writing, and the shared locks for reading, but you can also operate safely with only shared locks if you use some kind of more fine-grained access control for individual parts of a file, perhaps with an fcntl lock. The main difference is that rather than happening dynamically, and blocking, this kind of mutual exclusion can be applied statically.

Anyhow, for people who haven’t spent large amounts of time debugging obscure problems with filesystem locking APIs, that analogy may not be so helpful. But I do think that describing them as shared and exclusive (or unique), rather than (or in addition to) immutable and mutable, may be helpful for reinforcing the idea that the uniqueness of the reference is the real difference, but that uniqueness influences whether the compiler allows you to mutate the value directly because without some other protection in place, it can’t guarantee that doing so via a shared reference is safe.

1 Like

I agree with DanielKeep’s suggestion on how to phrase it: present interior mutability as an escape hatch. This is very familiar to C++ programmers since const is used a lot in that language but mutable provides an escape hatch. The presence of the mutable keyword does not make people question the concept of constness in C++.

Talking about “escape hatches” leaves me with a bad aftertaste. It has the connotation that Rust’s rules are badly designed and unreasonably restrictive, and so it also needs to have unprincipled workarounds to let you get out from under its own rules. Neither is true. Rust’s rules are very sensible, and the various types with so-called “interior mutability” work harmoniously with the rules, not against them.

We spent years confusing ourselves with the seductive, but false, notion that &mut and & are fundamentally about mutability versus immutability. Now that we’ve learned through hard experience that this is not true, we should consider the possibility of not spreading the same confusion among newcomers to the language. The fundamental characteristic of &mut and &, from which everything else flows, is that they provide exclusive and shared access, respectively, while ensuring safety. Turns out that if you have exclusive access to an object, it is always safe to mutate it (hence the name: &mut). While if you only have shared access, in the general case, it is not - so access through a shared reference will usually be immutable. But some particular types can guarantee the safety of mutation even through shared access. Cell and Atomic* can provide safe, shallow mutation, for the single- and multi-threaded case, respectively; and RefCell and Mutex use runtime checks to guarantee exclusive access, and can therefore translate shared & to exclusive &mut references for “deep” mutation, for the single- and multi-threaded cases, respectively.

The rules for other things, such as Rc only providing shared references to the contained object, because shared ownership is inherently, well, shared (except if you can show that the reference count is 1!), also all naturally fall out of this framework.

14 Likes

As a newbie myself, I would much rather be told the truth upfront. Making programmers completely blind is not good.

I believe knowing we don’t know something is much better than being unaware we don’t know something.

Thus, my vote goes to daggers everywhere.

I like the “shared” terminology much better. Shared references. It makes it clear why atomic integers have mutators that take &self, for example.

I think glaebhoerl hits the nail on the head right here. If I had to explain Rust to a friend, I would use this explanation.

The downside is that so much of Rust uses the language "mutable" and "immutable". While I definitely now think that the error

"error: cannot borrow immutable field self.field as mutable"

makes a lot more sense as

"error: cannot exclusively borrow shared field self.field".

that ship has probably sailed. So, an additional constraint is that you want to explain it to people who have to interact with Rust's language, which is "mutable / immutable" rather than "exclusive / shared". In that context, it almost seems better to skip the above explanation and avoid the inevitable "well, why & and &mut then, rather than & and &ex?" (other than totally looking like "Sex").

You learn a bunch from the discussion either way, though. Do discuss it! :slight_smile: