Structs' field-level mutability control at instantiation (not at definition)

Summary

Modify syntax to let you put mut in right hands when you instantiate structs to refine their mutability.

Syntax

The mutability of a itself but not of each a.*.

let mut a = A { x: 0, y: 1 };

The mutability of a.x.

let a = A { mut x: 0, y: 1 };

The mutability of each a.*.

let a = mut A { x: 0, y: 1 };

Examples

let a = A { mut x: 0, y: 1 };


a.x = 1;  // Ok
a.y = 2;  // Error


// Mutability doesn't change by the move because it's internal to the compound data.
let b = a;
b.x = 2;  // Ok


// Here `mut` simply declars `y` is mutable, is not a part of destructuring pattern.
let A { x, mut y } = a;  // Ok
x = 3;  // Error
y = 4;  // Ok


// `a` doesn't match the type `mut A` as it has an immutable field `y`
fn f(b: &mut A) {
  b.x = 2;
}

f(&a)  // Error


// The syntax for the corresponding type for `a`
fn f(b: &A { mut x }) {
  b.x = 2;
}

f(&a)  // Ok

Motivation

  • You can separate visibility and mutability. For now, visibility (pub) implies mutability. With this change, visible means readable but not necessarily writable (mutable).
  • You don't ever need to write get/set functions, nor wrap fields with Cell or RefCell.
  • Solve [Pre-RFC] read-only visibility.

Tuples

Mutability of a itself but not a.*s.

let mut a = (20, 20);

Mutability of a.0.

let a = (mut 20, 20);

Mutability of all a.*s.

let a = mut (20, 20);

Advanced

Field shadowing (holding values in fields only in a scope), if interesting.

let a.x = 2;

let a.0 = 30;

Let's say I use this feature to make a Vec whose len field is now always read-only. Do I need to use Vec<T> { len, ... } everywhere?

The fact that passing such value to a function needs to take into account mutability of its fields means these literals create new types. You're not proposing any syntax or trait for these types, so it will not be possible to use them in structs, function arguments, or return types. Is that intentional?

This type-based mutability is also a novel concept for Rust, and doesn't match what mut is currently doing. Currently mut is about bindings, not values. It's attached to names of variables, not the data behind them. Owned data is always mutable. In fact, in Rust immutable data doesn't exist. Any apparent immutability is only a temporary side-effect of how the data is accessed.

This is legal and essentially a no-op in Rust today:

let mut a = a;

but if immutability was a property of the type, then it would have to become some kind of type coercion.

This is also valid, because Rust doesn't have immutable data:

let a = A { x: 0, y: 1 }; // pretends to be immutable
{a}.x = 1; // can mutate, because moves break connection to "immutable" `a`

Personally, I'd rather remove current mut (not &mut) entirely from the language, because it's not part of the type system, it creates a false impression of immutability where none exists.

3 Likes

That said, I'd like to tweak that a bit. I think it'd be interesting to allow something like

struct MyVec {
    pub(!mut) len: usize,
    ... more private fields ...,
}

where it's only usable outside the module for read-only uses, not updates (like assignment of &muting).

2 Likes

While it's not something I'd encourage, this is possible thanks to deref coercion. There's a crate that does this via a macro. I'll let you guess who created it…

3 Likes

Only where it’s produced, most likely in its new function. Anywhere else after your Vec is instantiated, you do not have an option to make its len field mutable again except destructing and reinstantiating a new Vec.

I did propose the corresponding syntax to assert field-level mutability in the Examples above, though I’m sorry you may need to scroll down the code block to show the part.

// `a` doesn't match the type `mut A` as it has an immutable field `y`
fn f(b: &mut A) {
  b.x = 2;
}

f(&a)  // Error


// The syntax for the corresponding type for `a`
fn f(b: &A { mut x }) {
  b.x = 2;
}

f(&a)  // Ok

[/quote]

Currently mut is about bindings, not values. It's attached to names of variables, not the data behind them. Owned data is always mutable. In fact, in Rust immutable data doesn't exist. Any apparent immutability is only a temporary side-effect of how the data is accessed.

Yes, I understand it and that’s why I proposed instantiation (or binding) -time, field-level (or right-hand) mutability declaration instead of definition-time. Though why does mut slip into type annotations in my proposal? Because structs (or generally composition types) and function arguments are also in a way about variable bindings.

So if after that I need to pass a Vec to a function I can just use Vec<T>? Doesn't this contradict your example where you can't pass an A where an A { mut x } is expected?

Let me use your comment to make a different (to yours) but further proposal from my initial one, which is scoped mut(scope).

For example,

pub fn new(..) -> Self {
  mut(self) Vec {
    len: 0,
    buf: ..
  }
}

With this, Vec.* is now mutable from functions in the same scope but not from the outside. You can simply access len field by vec.len without len() getter function while avoiding changing it from outside (e.g. vec.len = 3).

You don’t need to modify the return type Self to mut(self) Self by the principle of ‘internal (right-hands) mutability doesn’t change by moving’.

Then how would these functions differ from the point of view of the caller?

pub fn new(..) -> Self {
  mut(self) Vec {
    len: 0,
    buf: ..
  }
}

pub fn new2(..) -> Self {
  Vec {
    len: 0,
    buf: ..
  }
}

pub fn new3(..) -> Self {
  mut Vec {
    len: 0,
    buf: ..
  }
}

I’ll try to answer your question, being not sure if you’d be satisfied with it.

  1. It’s a module owner who cares and is responsible for structs’ mutability they publish, not a user/caller.

  2. If you want to control (including choosing the scope of) the mutability beyond your own scope and yet be confident about possible unsafeness, then (, virtually speaking,) ask the owner to make each fields pub and then you instantiate the struct by yourself.

I feel like this part of the motivation reflects a somewhat poor understanding of why & and &mut (and by extension Cell and RefCell) exist in rust.

This is a topic that comes up time and time again, but &mut is really a misnomer. A far more accurate description of &mut is that it is a unique pointer; this is fundamental to rust's guarantee against data races, and it is also a precondition that unsafe code can rely on in order to reason about undefined behavior. (note this is only about &mut specifically; mut as in mut bindings is little more than a lint)

A lot of ink has been spilled on the topic by various people, but here's a couple of links:

What prevents the following code from invoking undefined behavior?

struct A {
    x: String,
}

fn f(arg: &A { mut x }) {
    // &T always implements Copy
    let copy: &A { mut x } = arg;
    // this borrows from copy, not from arg
    let str_copy = &copy.x;

    // push_str may reallocate the String
    arg.x.push_str("asdfghjkl");
    // and so this may print from deallocated memory
    println!("{}", str_copy);
}
  • We could try saying that &A { mut x } does not implement Copy, however, currently in rust, &T is Copy for all T (and generic code relies on this frequently). So A { mut x } would not work in generic functions.
  • We could say that the copy is still valid but the borrow checker "sees through it", looking more closely at the usages of the fields so that it can generate a borrow error at the call to push_str. However, this is easily thwarted with a slightly more indirect example:
fn hide_the_borrowck_error<T: Copy, U>(
    value: T,
    project: impl FnOnce(T) -> U,
    mutate: impl FnOnce(T),
    read: impl FnOnce(U),
) {
    let projected = project(value);
    mutate(value);
    read(projected);
}

fn uh_oh(arg: &A { mut x }) {
    hide_the_borrowck_error(
        arg,
        |r| &r.x,
        |r| r.x.push_str("asdfghjkl"),
        |str| println!("{}", str),
    )
}

This misuse cannot be detected without global analysis, which makes this idea a non-starter; the whole point of rust's lifetime annotations is that they let us take the global problems of aliasing and data races and replace them with local problems, which can always be reasoned about by looking at a single function.


rust is designed so that a change to the body of a function cannot cause any downstream code to stop compiling unless it also changes the signature. This provides a readily visible boundary between implementation detail and public API, and provides a decent baseline for library authors to decide whether a change requires a major semver bump.

In the eyes of a rust coder, your pub fn new3() -> Self { mut Vec { /* ... /* }} feels unfair; it feels like it hides information about the type because to us, "types" and "public API" are the same thing.

6 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.