Forever immutable owned values


#1

We have the concept of “exclusive” (or mutable) references &mut T in Rust. The only way to create value of that type in safe Rust is to (at some point) have a let mut foo: T = .. and then do &mut foo somewhere else.

Owning a value and not marking that mut in let mut foo = make_owned_value(); does not mean that it is not possible to mutate it. All you have to do is to move foo and now you are allowed to mutate the moved foo. You can simply do this with:

fn main() {
    struct Foo;
    fn make_owned_value() -> Foo { Foo }

    fn identity<T>(x: T) -> T { x }

    // not marked as mut:
    let foo = make_owned_value();

    // we got a 'mut' variable from a non-mut one by moving:
    let mut foo2 = identity(foo);
    
    // other stuff that mutates foo2 and uses foo2.
}

As an aside, I think therefore that it is morally right to call &mut an exclusive reference as @SimonSapin puts it, and not a mutable reference.

During a discussion on #rust with @withoutboats, @nox, et al. I had the idea of introducing freeze bindings which are truly immutable in the sense that once you’ve frozen an owned value, you can’t mutate it even if you move ownership. An example in code would be:

struct Foo(usize);

fn identity<T>(x: T) -> T { x }
fn id_freeze(
    // We acknowledge that `x` is frozen.
    // Or perhaps:  `x: freeze Foo`
    freeze x: Foo)
    // The type system must be able to trace frozen values,
    // so it becomes part of the type.
    -> freeze Foo
{
    x
}

// We can define `freeze`.
// This accepts non-frozen and frozen values and freezes them.
// This suggests that `T <: freeze T`, i.e: a `T` is a subtype of `frozen T`.
fn freeze<T>(x: T) -> freeze T { x }

let freeze foo = Foo(0);
let freeze foo2 = id_freeze(foo); // Legal.

let foo2_shared = &foo2; // Legal
let foo2_excl = &mut foo2; // Illegal
foo2.use_mut_receiver(); // Illegal

identity(foo2); // Illegal.
let mut foo3 = id_freeze(foo); // Illegal.

let foo = Foo(0);
let freeze foo2 = id_freeze(foo); // Legal.

// Frozen values are infectious inwards to fields.
// Let's pretend that Foo does not contain a copy type.
let freeze foo = Foo(0);
let mut inner = foo.0; // Illegal, assuming that typeof(foo.0) is not Copy.
let inner = foo.0 // Illegal, assuming that typeof(foo.0) is not Copy.
let freeze inner = foo.0; // Legal
// Legal since it flows from a &T shared reference:
let mut inner = foo.0.clone();

These are just some vague ideas at this point and may be wholly useless or very not nice to work with, but I thought I’d share the idea with y’all. @withoutboats also thought them related to the Pin story.


Show warning only when let variable is modified
Idea: DerefPin/DerefCell
#2

Seems botbot was dead whenever this discussion took place (here is the first post recorded in months, incidentally only hours before @Centril wrote this post. Is this fate?).

Can you share the use case for this? It seems to me to be taking a wart of the language (that mut really has nothing to do with mutability) and amplifying it for no apparent gain.


#3

Logbot recorded it tho, https://mozilla.logbot.info/rust/20180222#c14344459

None as of yet… just abstract thought sparked from the IRC conversation (“this might be interesting, I’ll write it down before I forget it…”) =P


#4

Would it be impossible to mutate the T in a RefCell<freeze T>? Just trying to get the general idea here.


#5

I do like the way that the “custom receiver types” lets us bootstrap these sorts of things. I wonder if we might use a similar approach for &uninit etc.


#6

That’s a great question that really makes you think!

Given that RefCell<T> allows interior mutation of the T inside via a &RefCell<T>, you should at the very least be able to mutate the inner T with a freeze RefCell<T>.

I think the semantics of RefCell<freeze T> depends on whether or not the impl<T> RefCell<T> already applies or not.

If it does, then syntactically get_mut should return &mut freeze T which would not allow you to mutate T. And DerefMut from borrow_mut would also give you &mut freeze T (effectively &T). replace would allow you to replace the frozen T with some other frozen T (or to-freeze T since T <: freeze T) and give you back a frozen T, but you may never get back something unfrozen from the RefCell<freeze T>.

However, the impl may not apply since T <: freeze T and not freeze T <: T. If so, I think then it is up to the standard library to decide whether or not you permit impl<T> RefCell<freeze T> (modulo specification syntax) and what the semantics of that are.

I assume you are referring to arbitrary_self_types? Can you elaborate a bit on how it allows us to bootstrap “these kinds of things”? =)


#7

You could model this with Freeze<T> that supported only Deref<T> (and perhaps an auto trait to rule out cells, if that bothers you).


#8

Oh yes, that should work out quite nicely with: https://play.rust-lang.org/?gist=7afc39081d99c3422b8b165bb06d2530&version=nightly

I’m not quite sure however how you can define the fn inner(self: Freeze<Foo>) -> Freeze<Inner> transformation tho without using an unsafe block in it (which I guess you could use, but that becomes a lot of unsafe blocks). I don’t see how you can get the nice let freeze inner = foo.0; behavior =)


#9

perhaps freeze String literals?

e.g. String instances which don’t require run-time memory allocation by pointing to a character array sitting in read-only data portion of the binary?


#10

I don’t see any use case for such a type that wouldn’t be better served by &'static str. Since freeze String is not interchangeable with String, code would have to be specifically written to use freeze String instead of String and thus could just as well be specifically written to use &'static str. The only method available on &String that isn’t on &str is capacity, which is pretty useless on its own.


#11

I have a few questions.

  1. Wouldn’t this change the core definition of what it means to own a value? A definition change sounds like a bad idea to me, due to the increased complexity alone. We don’t want Rust to become Rust++, after all.

  2. What is the use case for a permanently frozen value? What can be done with that that you cannot do with a regular owned value? Or alternatively, why would or should anyone care about permanently freezing the value?

As an aside, I’ll stick with calling &mut mutable references. The reason is simple: if we want to be pedantic about it, we should call them neither mutable references nor exclusive references, but mutable exclusive references, or exclusive mutable references, or something like that. That’s just too much of a mouthful in the same way GNU/Linux is, never mind that it should then actually be GNU/Linux/Xorg/Systemd/…, you get the point.

Mutable references is the known term and looking at term evolution in societies in general but especially in technology-oriented societies, it’ll likely stick around at least as a well known synonym. That automatically turns it into the preferred term for me.


#12
  1. I believe the current notion of ownership would not be changed, but a parallel one would exist along side the current. I’ll leave whether this makes Rust into Rust++ unsaid - no breaking changes would occur as a result of adding freeze. Instead, it is purely an addition.

  2. This remains to be seen =) To be clear, I’m not proposing we add freeze to the language at this stage, for me it is mostly academic. For such a large change to be considered I believe we would need to find a significant number of use cases that are facilitated by this change. And right now, I can provide no such cases.

Regarding naming of mutable/exclusive references, I think that when a bunch of experienced rustaceans are hearing either “exclusive” or “mutable” references they can tell what is meant, or even “mut refs” for slang.

However, as a matter of teaching Rust, I will take your non-proposal to heart and propose that we should communicate by saying:

  • exclusive mutable references
  • shared immutable references

I believe this succinctly tells listeners and readers about what the problem is and how Rust solves it.


#13

No such a change alone would not turn Rust into Rust++. I’m never too concerned about any single change to the language, if it’s accepted by the core team I imagine there’s not too much damage it can do. Instead I try to look at new proposals in terms of the cost to the complexity budget* and what that amount of complexity actually buys the ecosystem at large, and that’s also where my questions came from.

Glad I could help without intending to :smile:
In fact, it indeed is a good idea to use those full phrases as a matter of teaching Rust. Just not in regular conversation.

*Everybody has a finite mind, and for all its flaws C is a relatively simple language in that you can hold all its features in your head at once. By that definition, C++ is not simple, and isn’t getting any simpler as time moves forward. Rust is somewhere in the middle, as it certainly has more features than C. However, while it can often do comparable things to C++, and with more safety, it is a much smaller language.


#14

Totally with you there, I think that’s the right way to look at things. One should also consider how coherent new additions are compared to the current language - if they are coherent and simplify use cases pervasively or make the language more consistent, I believe (mental) complexity can even be reduced by new additions.

Accidental help is the best =P