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.
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.
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.
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)).
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.
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.
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.
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.
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.
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.
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.
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!