TLDR: It seems to me strong updates would be quite beneficial and might not be "too hard" to provide given what Rust already provides. This thread aims at gauging where the community stands in that regard. (Is it something that could be considered 10 years from now? Or is it something better suited for a new language?)
I call strong update the operation of assigning both a value and a type to a place, while weak update only assigns a value (which must thus have the same type). For example:
fn main() {
let mut foo = MaybeUninit::uninit();
// Here, foo has type MaybeUninit<Foo>.
Foo::new_in_place(&mut foo);
// Here, foo has type Foo.
foo.process();
}
impl Foo {
// The mutable reference has type MaybeUninit<Foo> now and
// type Foo at the end of the borrow. See the unsafe mental model
// for more information.
fn new_in_place(foo: &mut [MaybeUninit<Foo> .. Foo]) {
*foo = Foo;
}
fn process(&self) {}
}
Link to the unsafe mental model, in particular the mutable reference section.
Strong updates would benefit MaybeUninit
, Pin
, NonNull
, NonZero
, and any other repr(transparent)
types, where wrapping types are used to track properties of the wrapped value. Those properties could be modified in place without copy or move (and without relying on the compiler to optimize such copy/move). Type states are a general category of such "wrapping types adding/removing static properties", replacing by-value functions like fn close(door: Door<Open>) -> Door<Closed>
to by-ref functions like fn close(self: &mut [Door<Open> .. Door<Closed>])
.
Design questions:
- The new type must have the same layout as the old one. This is a constraint of using the same place.
- How to deal with control flow branches (think strong updates inside if-branches)? The abstract interpretation answer would be to use the least upper bound (with respect to subtyping), but a simpler initial solution would be to require the type to be the same when control flow joins. See first example below.
- Moved-out places could have a "no access" type and could thus be rassigned later. See second example below.
- Modifying the type of a place has a recursive effect to its container stack. For example changing the type of a field with a generic type, would also change the associated type parameter of its containing struct (and so recursively up to the allocation containing that place). This action-at-a-distance might initially be surprising.
let mut x: MaybeUninit<i32> = MaybeUninit::uninit();
if b {
x = NonZero::new(42).unwrap(); // x: NonZero<i32>
} else {
x = 13; // x: i32
}
// This could be rejected because x has different types.
// Or this could be accepted by using i32 as the least upper bound of
// NonZero<i32> and i32 (since the former is a subtype of the second).
Example for moved-out places (not convinced it's useful, but it's possible). Strong updates is just a generalization of moved-out places.
let mut x: NotCopy = NotCopy::default();
consume(x);
// Now x could have type NoAccess (which provides to valid operations).
x = MaybeUninit::new(NotCopy::default());
// Now x has type MaybeUninit<NotCopy> and can be used again.