[Pre-RFC] Extend mutability control to control partial moves of objects


#1

SUPERSEDED:

The formal version of this RFC is up: RFC PR 238. The formal RFC supersedes this pre-RFC, and makes the other pre-RFC unnecessary.

Summary

Extend Rust’s mutability control semantics, so it also controls whether an object can be partially moved.

Motivation:

Currently, there is no way to restrain movability of Rust objects with move semantics - all movable objects can be moved at will. But it may be beneficial to have some control over movability, like how we have control over mutability.

EDIT: it is not correct to say that there is currently no way to restrain movability in Rust. For one, immutable objects do not accept "partial move-in"s. But they accept "partial move-out"s. This asymmetry is problematic. Becides, aren’t partial move-outs mutations?

This proposal is one of my two proposals on movability control (the other being that one). This one is intended to control the following kind of movability: the ability to partially move an object. Or, to solve the problem that “immutable” objects are not immutable enough.

In today’s Rust, it is valid to partially move an object in an immutable variable:

use std::mem::drop;

#[deriving(Show)]
enum Gender {
    Male,
    Female,
}

#[deriving(Show)]
struct Person {
    pub name: String,
    pub gender: Gender,
}

fn main() {
    // Mike is stubborn that he will not change his name,
    // or ever treat himself like a woman.
    let mike = Person { name: "Mike".to_string(), gender: Male };
    // However, we can erase his name without him noticing.
    drop(mike.name);
    // Now he is a nameless person, but he still has a gender.
    println!("This nameless person is a {}.", mike.gender);
}

The above snippet compiles and runs, but it goes against intuition.

Detailed Design:

Definition:

  • top-level object: a top-level object is the root of an object tree which inherits the top-level object’s mutability.
  • there are two kinds of top-level objects, those stored directly in variables, and those stored directly in objects with internal mutability.

Add the following rules to Rust’s mutability semantics:

  1. if an object is immutable, then all its sub-objects with move semantics become immovable individually;
  2. if an object is mutable, then all its sub-objects with move semantics become movable individually;
  3. the movability of top-level objects is unconstrained, i.e. both immutable and mutable top-level objects can be moved as a whole.

(Note: Movability control of top-level objects is the subject of the other proposal.)

Thus, the above code snippet would cause a compile error on the drop call, telling us we cannot partially move an immutable object.

Drawbacks:

  1. breaking change;
  2. increased implementation complexity.

Alternatives:

Maintain the status quo.

The status quo may be fine, as once an object gets partially moved, it’s a compile error to use the moved parts or the object as a whole, so partial moves of immutable objects may not be a serious problem.

Still, the fact that we can move away parts of an “immutable” object and render it unusable is quite against the definition of “immutable”, and we’d better have less surprise in the language.

EDIT: And the scoped lifetime guarantee in the other proposal requires guaranteed partial-immovability. This is another point for the change.

Unsolved Questions:

None that I am aware of.


#2

(Note: I’m not relating this RFC with the scoped RFC, instead just taking this individually.)

One concern is that a lot of methods which take self will need to be modified to say

fn into_something(mut self, ...) -> Something { ... }
//                ^^^

Though this could be done with mechanical grep-and-replace.


Question.

Will this affect pattern matching?

match mike {
    Person{name:m, gender:Male} => { println!("Male = {}", m); }
    Person{name:f, gender:Female} => { println!("Female = {}", f); }
}

Or

let x = (box 1i, box 2i, box 3i);
let (p, q, r) = x;

Alternatives

What about extend the “partially moved value” checking:

  1. When a partial move-out happens, the immutable object becomes a state of “partially moved value”.
  2. Calling method or taking reference on a “partially moved value” is an error — as already happening now.
  3. Reading members from a “partially moved value” is an error. — new
  4. Moving unmoved members from a “partially moved value” is still valid — as already happening now.
let mike = Person { name: "Mike".to_string(), gender: Male };
drop(mike.name);
// ^ OK on this line
println!("This nameless person is a {}.", mike.gender);
// ^ error: use of partially moved value: `mike`

#3

Thanks. I think I should have mentioned in Drawbacks that we’ll be writing more mut (and ref).

But personally I consider this a plus.

About your pattern matching examples, both were doing moves which the programmer might not be aware of. I’d like to argue that at least in the first example, moves were not what he/she wanted, Mike became a nameless person after his name got printed. refs should have been added, even without my proposed changes. I think my proposal helps finding out these kinds of bugs.

And the second one can be fixed by doing let mut x = ....

I think your alternative does not solve the problem that “immutable” objects are not immutable enough, and having something that can be moved/copied but not read is more surprising.