Edit: I'm adding this brief summary to make the suggestion more clear.
Rust has a modifier &mut which is restricted so that only 1 may exist at a time, per variable part, but there are no restrictions on how the target may be modified.. On the other end of the spectrum would be a modifier for which any number of them may exist at a time, but the restriction is instead on how the target may be modified to keep things safe.
So I recommend creating &nmut. Any number of these references may be created to refer to any type of variable. Only the Copy parts of the target may be written to, which makes this safe and easy to verify at compile time. Thus making multiple references easy to use, easy to understand, and very clean code.
Some have suggested Cell can do this. Only for very limited types, and even then their use is extremely ugly and tedious and violations of good programming standards (like open/close principle) and generally not something any programming language should ever force onto any programmer.
Suggestion: in addition to mut, add nmut to represent "in place mutation". An in place mutation is one that is allowed to do any write except for those that could invalidate a reference.
Writing to a usize is an in place mutation. Pushing or popping from a vector is not, as a push could result in the vector's buffer being moved or a pop could result in a reference to the last element being invalidated. In:
let a = SomeStruct { field1 : 7usize, field2 : Box::new(8usize) }
then a = x is not an in place mutation, because it could invalidate a reference to field2's target. But a.field1 = 9 would be an in place mutation.
As far as the rules for constructing an &nmut, they can safely be far more relaxed than the rules for making &mut. It should be safe to write let a = &nmut b.c; let d = &mut (*a).e.f . No modification to a should be able to invalidate d. It might even be the case that the rules for &nmut can allow for them to be treated as & (ref consts) as far as when they are allowed to be created. The following would be fine:
fn main() {
let nmut a = 7usize;
let b = &nmut a;
let c = &nmut a;
println!("{} {} {}", a, b, c);
*b = 8;
println!("{} {} {}", a, b, c);
*c = 9;
println!("{} {} {}", a, b, c);
}
Pros:
Less unsafe code. Many algorithms require multiple mutable references, so some rust programs use unsafe code pointers to change the mutability of a pointer. Or they an encoding to represent a path into part of a variable, like saving an index into a collection instead of having a reference to the respective element of the collection. The problem being that these encodings can't be checked at compile time like references. Also parsing these encodings is slower than just dereferencing a pointer.
More readable code. Instead of declaring things as immutable but using unsafe code to modify it, just say it for what it is.
Backwards compatible. This is a conservative extension to the language. Possibly some &mut inputs into standard library functions could be changed to &nmut, or in place alternatives would have to be added to get the full benefit of this addition, but it doesn't have to be done immediately.
Cons:
Increased complexity to the language. This a step towards "improving things until they become incomprehensible". But I think "you can mutate this variable as long as you don't do anything that could invalidate a reference" is pretty easily understandable.
Might need different rules for multi threaded code. I'm not very familiar with Rust's multi threading mutation rules so someone else would have to comment on this.
Less readable code. Forcing code to only have 1 source for the mutation of any piece of data makes things more readable. But on the other hand, when you do actually need 2 references that benefit goes away.
More cases to write out. Rust already has a problem of programmers having to write
f(v : T) -> T
f_ref(v : &T) -> &T
f_mut(v : &mut T) -> &mut T
for things like iterators or some trait functions like indexing. Having to add one more, like
fn index_nmut(&nmut self, index: Side) -> &nmut Self::Output {
is making that problem a little more annoying than it already is. However, that needs to get fixed regardless of whether nmut is introduced or not.
I looked an didn't see this suggested yet, but it wouldn't surprise me if it has. I am wondering if someone on the verification part of things can say whether this could be introduced without breaking anything.
What happens if b and c are sent to different threads?
use crossbeam::scope; // 0.7.3
fn main() {
let mut a = 5;
let b = &mut a;
//let b = &nmut a;
//let c = &nmut a;
scope(move |s| {
let ah = s.spawn(move |_| {
*b = 10;
});
let bh = s.spawn(|_| {
//*c = 15;
});
ah.join().unwrap();
bh.join().unwrap();
}).unwrap();
dbg!(a);
}
Couldn't this be solved with Pin (https://doc.rust-lang.org/std/pin/index.html)?
From the docs:
A
Pin<P>ensures that the pointee of any pointer typePhas a stable location in memory, meaning it cannot be moved elsewhere and its memory cannot be deallocated until it gets dropped. We say that the pointee is "pinned".
This _exact_ suggestion may be new, but it's extremely similar to a wide range of past suggestions for "new/different borrow checker rules/reference types" which on close scrutiny don't enable anything fundamentally new, even if they sound like they should. https://users.rust-lang.org/t/alternative-to-mutability-restriction-in-single-threaded-contexts/40906 is a recent example of someone falling into a similar trap.
Unfortunately there's no easy way to discover this via searching, because most of them boil down to misunderstanding or oversimplifying how/why Rust works today, or not noticing some existing library solutions (like Pin). Case in point: IIUC the only way this &nmut could be sound _and_ actually allow mutation is to essentially reinvent all of the "interior mutability" rules that & references already have (which is the point of kenny's thread question).
&nmut T is no different from &Cell<T>, and given that you can create references to a cell with Cell::from_mut, even if you haven't wrapped the pointee in a cell, there isn't really any benefit to &nmut T.
Thanks for the responses. I was slow in responding because I wanted to make sure I took a the time to read and understand them and think about them.
I hope this example clarifies things. Consider if you have a collection and you want to write the first element into the last element. Even though this example is only using a slice, please think of it as using some very complicated container data structure:
struct Person {
name : String,
age : usize,
}
fn replace_first_with_last(party : &mut [Person]) {
let last = & party[party.len() - 1];
let first = &mut party[0];
first.age = last.age;
}
Rust won't accept this code because it has both a &mut and a & (const) to the same variable. Rust slices have 2 ways of getting around this problem. The first would be to use indexes, as
fn replace_first_with_last2(party : &mut [Person]) {
let n = party.len();
let first = 0;
let last = n - 1;
party[first].age = party[last].age;
}
Here it is in effect encoding the 2 first/last references as 3 things: 1 reference to party and 2 codes (the indexes). This is only feasible with arrays, if the collection was a Tree or a Graph then the codes would have to be entire stacks of indexes. And worse, it bypasses Rust's compile time safety checks. What if party instead owned its data instead of being a &mut, then after first and last are created, party shrinks the buffer, causing last to be in effect the encoding of a dangling pointer? That is:
let mut party = some_vector();
let first = 0;
let last = party.len() - 1;
party.pop().pop().pop();
party[first].age = party[last].age;
Using encodings of references instead of actual references is fundamentally unsafe code, even though the compiler allows it.
The other alternative I'll mention because it is known is
fn replace_first_with_last3(party : &mut [Person]) {
let (car, cdr) = party.split_at_mut(0);
let first = &mut car[0];
let last = & cdr[cdr.len() - 1];
first.age = last.age;
}
The first problem is that this is just a hack that is available for slices. It isn't something to expect a Graph or a Tree or a Queue some other complex container to support. Furthermore it removes access to the original array party, so even later saying "what is the sum of the ages of the people in party" is no longer possible.
What I am proposing is:
fn replace_first_with_last4(length : usize) {
let nmut party = (0 .. length).map(|e| person(e)).collect::<Box<[Person]>>();
let first = &nmut party[0];
let last = & party[party.len() - 1];
first.age = last.age;
//first.name = "replace".to_owned(); // this would be a compile error
}
(Changing party from a parameter to an owned variable to make the difference more distinct). Because party is declared as nmut, resizing or reallocating it is not allowed because that could invalidate references. Similarly first.name couldn't be changed because there could be an outstanding & reference to it's internal &str. This I think is a necessary safety guaranteeing zero cost abstraction. And it is much safer than users using unsafe code to bypass mutation or encodings to try to reinvent references.
What happens if
bandcare sent to different threads?
As I specifically said in my original post, thread rules would have to be addressed by someone with specific knowledge of Rust's thread requirements, I don't want to comment on that. Threads require atomic writes and attention being paid to write/read order, which is handled explicitly in single threads. Worst case scenario is that you just wouldn't allow nmut to be passed between threads until the rules are determined, if ever. Similar to how RC isn't allowed between threads.
Couldn't this be solved with
Pin(https://doc.rust-lang.org/std/pin/index.html)?
I am not terribly familiar with Pins, but it appears 'no'. First of all, what I am talking about is a compile time safety guarantee. The examples for Pin all seem to require unsafe code to use it.
Anything that uses unsafe code or requires "safe as long as you just don't use my API incorrectly" is not even in the ballpark of what I am proposing.
Second of all, pin doesn't actually guarantee that a write won't invalidate pointers does it? Like, if I had a x:Pin<Person> , is Pin smart enough to allow x.age to be written but deny a write to x.name? I expect it either denies both, in which case it is overly restrictive just like &mut, or it allows both in which case it allows for dangling pointers.
What I'm proposing might make 'Pin' obsolete. Rather than being a hint to the programmer "I intend to not move this" it actually enforces that no moves can be done period while an outstanding &nmut exists.
&nmut Tis no different from&Cell<T>, and given that you can create references to a cell withCell::from_mut, even if you haven't wrapped the pointee in a cell, there isn't really any benefit to&nmut T.
This is very different than a Cell. First of all, Cell is only defined for Copy data. Second, Cell panics at runtime, so it is definitely not safe to use. Like I said above, what I am proposing is a zero cost abstraction compile time safety guarantee.
Third, the purpose of Cells is to represent "internal mutation", not external mutation. For example, a cell could be used to cache the result of a function call. Using a cell for external mutation is very bad coding style. The purpose of cells is so that you can write
let a = Thing::new()
let b = Thing::new()
assert!(a == b)
println!("{}", a.f());
assert!(a == b)
...even f writes to some register of a that is wrapped in a Cell, such as a cache. A call to f should not modify any call to eq(). It is incorrect to use a cell in a way that violates Leibniz Law https://en.wikipedia.org/wiki/Identity_of_indiscernibles . That is what is meant by "internal" mutation. My 2 cents, it is unkind to those who read code to use a Cell to just arbitrarily change a const to a mut for externally visible mutations, they should be alerted of that with unsafe so to make the violation of mutability rules explicit.
It is the purpose of nmuts to change data in a way that changes the effect on function calls on that data. That is, external mutation.
This _exact_ suggestion may be new, but it's extremely similar to a wide range of past suggestions for "new/different borrow checker rules/reference types" which on close scrutiny don't enable anything fundamentally new, even if they sound like they should. https://users.rust-lang.org/t/alternative-to-mutability-restriction-in-single-threaded-contexts/40906 is a recent example of someone falling into a similar trap.
I am pretty certain that what I am proposing is new in the sense of "does not exist in the compiler and cannot be emulated with crates". But it probably isn't new in the sense of "I'm not the first person to realize this." The person you linked to noticed the same problem I did, that Rust denies writes that don't invalidate pointers when it shouldn't. I strongly dislike his proposal for fixing it, as you seem to also.
I are talking about is that the calling code and the implementing code can each be compiled completely independently of each other, using nothing but the function signature and whatever types/variables are in each scope.
I agree with most of this. To be fair to that poster, while it is true that Rust only looks at the contract to called functions for verification, it is also true that Rust looks deep into functions to modify those contracts. For example, in
fn print_this<'a>(x : &'a str) {
prinln!("{}", x);
}
Rust will silently add a for <'a> to the function signature. What is wrong with his proposal is that it was yet another hidden piece of meta information that the compiler tags to variables that we just have to hope it does right and a casual programmer can't possibly "see" what is happening if the compiler correctly or incorrectly disagrees with him.
What I am proposing is explicit, not inferred by the compiler. If something is nmut, then only writes to Copy parts of it are allowed. mut can be downgraded to nmut. Multiple nmuts and consts references to a variable are no problem. Etc.
Unfortunately there's no easy way to discover this via searching, because most of them boil down to misunderstanding or oversimplifying how/why Rust works today
This is entirely possible. The Rust system is really complex and some gotcha might exist.
is to essentially reinvent all of the "interior mutability" rules that
&references already have
The entire point of Rust, as advertised, is zero cost safety abstractions. Not being able to implement an algorithm correctly because Rust's mutation rules are too strict is a huge cost. Both in terms of time to make a work around and in terms of runtime to use that work around.
It is a fact that programmers are using unsafe code to violate Rust's overly restrictive mutation rules. Code that can panic is not safe code either. I saw this video after I originally posted this: https://www.youtube.com/watch?v=enLUX1TtNyE about Actix being split between the maintainer wanting to use unsafe blocks and contributors wanting to use panicking alternatives instead. I haven't looked at the source code, but it is possible that more correct mutation rules could have avoided this fight.
I think this proposal would reduce a tremendous amount of unsafe and panicking code in Rust with straight forward shallow compile time checks. And it is backwards compatible and can be implemented progressively. Again thanks for comments.
Code that can panic is not safe code either
Let's get this out of the way first, Rust has a very specific definition of safe, programs must be "memory safe". Panicking cannot corrupt memory, so it is safe, even if it is not desirable, in the same way that leaking is undesirable, but cannot corrupt memory so it's safe (Box::leak is safe).
This is very different than a Cell. First of all, Cell is only defined for Copy data. Second, Cell panics at runtime, so it is definitely not safe to use. Like I said above, what I am proposing is a zero cost abstraction compile time safety guarantee.
Wrong on both counts, Cell can be used with any type, and none of Cell's methods panic. To use Cell with types that aren't Copy you can use Cell::replace. If you're type implements Default then you can also use Cell::take to extract the value from the Cell.
Like I said above, what I am proposing is a zero cost abstraction compile time safety guarantee.
Like Cell. Cell has exactly zero cost over not using Cell. It just prevents bad behavior at compile time by disallowing references to the wrapped value.
Third, the purpose of Cells is to represent "internal mutation", not external mutation. For example, a cell could be used to cache the result of a function call. Using a cell for external mutation is very bad coding style. The purpose of cells is so that you can write
The purpose of Cell (and anything built on top of UnsafeCell) is shared mutation, which is exactly what you are proposing. (what does "internal mutation" even mean? If you are referencing the term internal mutability, that's a pretty ill-defined term so I wouldn't touch it with a 10 foot pole).
There is nothing wrong with using Cell if you need shared mutability, it's just that you usually don't need shared mutability. (I think it would be helpful if you read Rust a Unique Perspective to better understand how Rust's references work)
What I am proposing is:
fn replace_first_with_last4(length : usize) { let nmut party = (0 .. length).map(|e| person(e)).collect::<Box<[Person]>>(); let first = &nmut party[0]; let last = & party[party.len() - 1]; first.age = last.age; //first.name = "replace".to_owned(); // this would be a compile error }
Why does the first.name = ... fail to compile? Also, what happens if length == 1.
then first and last would alias.
What I am proposing is explicit, not inferred by the compiler. If something is nmut, then only writes to Copy parts of it are allowed.
Ahh this is why, so this is more restrictive than &Cell<_>. Why not just use &Cell<_> then?
My 2 cents, it is unkind to those who read code to use a Cell to just arbitrarily change a const to a mut for externally visible mutations, they should be alerted of that with unsafe so to make the violation of mutability rules explicit.
This shows a fundemental misunderstanding of what references are in Rust. The mutability rules for references are just safe defaults. Nothing more than that. Yes, if you are using shared mutability through Cell or otherwise, you should make that known in the documentation, but that's not a reason to add a whole new reference type. &nmut doesn't fundamentally add anything new over the current situation.
Multiple nmuts and consts references to a variable are no problem. Etc.
So would this be fine?
let mut x = 0;
let nmut_x = &nmut x;
let const_x = &x;
*nmut_x = 1;
assert_eq!(*const_x, 0);
If not, why? And why are slices treated differently?
I agree with most of this. To be fair to that poster, while it is true that Rust only looks at the contract to called functions for verification, it is also true that Rust looks deep into functions to modify those contracts. For example, in
Nope, lifetime elision only looks at the function signature, not the function body. If you are only given the function signature, and the type definitions involved, you can figure out all elided lifetimes.
I am not terribly familiar with Pins, but it appears 'no'. First of all, what I am talking about is a compile time safety guarantee. The examples for Pin all seem to require unsafe code to use it.
Anything that uses unsafe code or requires "safe as long as you just don't use my API incorrectly" is not even in the ballpark of what I am proposing.Second of all, pin doesn't actually guarantee that a write won't invalidate pointers does it? Like, if I had a x:Pin
, is Pin smart enough to allow x.age to be written but deny a write to x.name? I expect it either denies both, in which case it is overly restrictive just like &mut, or it allows both in which case it allows for dangling pointers.
I agree with you here, Pin seems to have different goals than &nmut. Pin is about guaranteed immovability, wheresas &nmut is about shared mutation.
about Actix being split between the maintainer wanting to use unsafe blocks and contributors wanting to use panicking alternatives instead.
That's off topic, because Actix had a number of different reasons for using unsafe blocks, and many of them would not have been changed by adding a &nmut.
I started reading your response and got to this point:
Because party is declared as nmut, resizing or reallocating it is not allowed because that could invalidate references.
These concepts don't exist to the compiler. One of your motivations for this feature was to make it easier to work with more complex and user-defined types (ie. not just slices) but there's no way for the compiler to know what "resizing" means to a BTreeMap.
You can implement the Index however you want, and that could include invalidating references.
Also, as others have said, what you are trying to achieve is already possible by using interior mutability, eg. the Cell type.
For completeness, your example written with Cell:
use std::cell::Cell;
struct Person {
name: String,
age: Cell<usize>,
}
fn replace_first_with_last(party: &[Person]) {
if let (Some(f), Some(l)) = (party.first(), party.last()) {
f.age.set(l.age.get())
}
}
For completeness, your example written with
Cell:
Here is why I disagree with this suggestion:
(1) This requires being the maintainer of the crate defining the Person struture to change age from usize to Cell<usize>. That is often not the case. nmut doesn't have this restriction.
(2) Now it impossible to have a constant version of Person anywhere. In order to understand code, you'd have to look into the body of every single function that uses Person to see if it is mutated. This version of replace_first_with_last is an example of a "secret" modification of a & variable. Below I talk about how nmut rules can be chosen to deny these secret modifications.
(3) This violates the Open-Closed Principle. You are going back and modifying Person, an already complete definition, just so that multiple references to it can be made. Sometimes violating O-C is unavoidable, but here it is completely avoidable with nmut and the need for mutable references is far too common for a language to force you to violate O-C.
To use Cell with types that aren't Copy you can use Cell::replace
Replace with what? into_inner requires ownership, take requires Default (and if you used it for something like a BTree it would at least temporarily invalidate the tree) and rewrites the entire data structure when possibly only a single small modification was needed, and get requires Copy. The nightly function update looks promising, but that only lets you rebuild the Cell in terms of itself. So for the example, even update wouldn't allow you to replace the first age with the last age.
This is very different than a Cell. First of all, Cell is only defined for Copy data. Second, Cell panics at runtime, so it is definitely not safe to use.
Wrong on both counts,
Cellcan be used with any type, and none ofCell's methods panic.
My memory of where the Copy requirement is in Cell was off, but my point was correct. To get a reference to cell data it has to be Copy, and to do so with RefCell requires a panic check specifically to prevent multiple references.
Why not just use &Cell<_> then?
Can you implement my example in a way that doesn't take advantage of the specific type (not Copy, not Default, not owned, not one that you are the maintainer of) ?
Why does the
first.name = ...fail to compile?
Because name is a std::string::String which does not implement Copy.
I started reading your response and got to this point:
Because party is declared as nmut, resizing or reallocating it is not allowed because that could invalidate references.
These concepts don't exist to the compiler. One of your motivations for this feature was to make it easier to work with more complex and user-defined types (ie. not just slices) but there's no way for the compiler to know what "resizing" means to a BTreeMap.
Depends on if you are using unsafe blocks or not.
If you are not using unsafe blocks, then the compiler can easily enforce that only Copy parts of an nmut (or &nmut) are written to. And that is how the compiler would know whether you are doing something that could invalidate a pointer, such as resizing. All compile time verification works like this: if all the parts are verified, then the composition of the parts are verified. I'm not suggesting anything different. Example: if someone tried to implement a linked list using only safe code, and tried to implement List<T>::push(&nmut self, new : T) it would be compile-time impossible because at some point push would have to modify a non Copy value like a Box. But List<T>::index_nmut(&nmut self, index : usize) -> &nmut T would allow you to get as many in-place-writable references to the nth-data as you want, no compiler restriction.
For unsafe code blocks, they'd have to be careful around nmuts. It would be possible to write unsafe code that would break nmut rules; for example, because Vec::pop only needs to modify a Copy part of the vector (decrementing the length), it would be possible to define it as Vec::pop(&nmut self), but that would be mistake in the API not catchable by the compiler. And it is only possible because of the unsafe blocks Vec uses to operate on its buffer.
I'm not suggesting something the compiler couldn't do, to the best of my knowledge.
The purpose of
Cell
From the docs
In this example, you can see that
Cell<T>enables mutation inside an immutable struct. In other words, it enables "interior mutability".
And all the examples are of interior mutability, like cache and the counters in RC. Formally, interior mutation refers to a write that doesn't change the output of functions satisfying the Indiscernibility of the Identity. Informally, it means you could have implemented the function without doing a mutation, but for whatever reason (optimization) you do otherwise.
Maybe Cell was intended from the start to be a way to a way to share mutable pointers. The std::cell docs suggest maybe so. But I think that a library that lets you modify the target of a & is something that should only be used when that modification is of no functional consequence (interior mutation), and that using it as a substitute for &mut is, in my opinion, kinda stinky. Technically works in some cases but not pretty when it does.
So would this be fine?
let mut x = 0; let nmut_x = &nmut x; let const_x = &x; *nmut_x = 1; assert_eq!(*const_x, 0);
Well in the first line you'd have to declare x as let nmut x = 0;, so that the second line wouldn't hide x from the third line.
But I think your main point was aliasing, and it is a good point. What I said before didn't explicitly deny having both a & and a &nmut to the same data, but that is something that could be done the same way it is done for &mut.
This allows for shared references using &nmut that still enforces the "& doesn't change" rule. Which is something that Cell does not do: when cells are introduced to a type, you can no longer trust that & are constant. And that is fundamentally new, and a major help for code readibility.
(I think it would be helpful if you read Rust a Unique Perspective
Hmm, what he says about optimization, I've specifically heard a contradictory claim from someone at a Rust Conf speaking on that subject. I myself don't know who is right, and it is definitely important; I think I'll open a stack exchange question on it as it is a bit off topic of this discussion.
Let's get this out of the way first, Rust has a _very_ specific definition of
safe, programs must be "memory safe". Panicking cannot corrupt memory, so it is safe, even if it is not desirable, in the same way that leaking is undesirable, but cannot corrupt memory so it's safe (Box::leakis safe).
That is the standard that is used when defining whether a library function is tagged as unsafe.
Rust has a lot of great features, but it has 1 main feature that makes it worth being a new language. That is the compile time verified reference system. It verifies, for code that doesn't use unsafe, that references are not pointing to invalid data, not double freed, and not leaked with no runtime overhead. And it does that without invaliding the entire program, e.g. panicing. That is why Rust, the language itself, can be advertised as safer than C/C++. The compiler's verifier doesn't verify other things (like array indexing or unsafe blocks), which is why library functions have to allow panicing code to be labeled safe, otherwise almost every library function would be labeled unsafe.
But one of the sacrifices made to obtain those claims is that only 1 reference at a time has rights to mutate. I think allowing another kind of reference, one that allows for multiple mutating references to the same data but can only modify Copy data is a natural improvement.
(1) This requires being the maintainer of the crate defining the Person struture to change age from usize to Cell
. That is often not the case. nmut doesn't have this restriction.
Not necessarily, you can convert a &mut T to a &Cell<T> using Cell::from_mut. This way you can temporarily share references as needed, but you function still needs to be marked &mut. Yes, if you want shared mutability across multiple functions, then you will need to change the type, but that's nothing new. That just means that your type requires shared access, so you design should reflect that.
In any case, adding a new reference type is a huge change, and there doesn't seem to be enough motivation to justify this change.
On a separate note: if &Cell<T> allowed for field projection, we could write something like this:
use std::cell::Cell;
struct Person {
name: String,
age: usize,
}
fn replace_first_with_last(party: &mut [Person]) {
let party = Cell::from_mut(party).as_slice_of_cells();
if let (Some(f), Some(l)) = (party.first(), party.last()) {
// imaginary Cell::project
Cell::project(f, Person.age).set(Cell::project(l, Person.age).get())
}
}
Which wouldn't be too bad. I guess there could be sugar on top of this to make it nicer.
Right now the best we can do is some tricky unsafe code with the nightly feature raw_ref_op
#![feature(raw_ref_op)]
use std::cell::Cell;
struct Person {
name: String,
age: usize,
}
impl Person {
fn age_nmut(this: &Cell<Self>) -> &Cell<usize> {
unsafe { &*((&raw mut (*this.as_ptr()).age) as *const Cell<usize>) }
}
}
fn replace_first_with_last(party: &mut [Person]) {
let party = Cell::from_mut(party).as_slice_of_cells();
if let (Some(f), Some(l)) = (party.first(), party.last()) {
Person::age_nmut(f).set(Person::age_nmut(l).get());
}
}
Given this, I don't see how &nmut T is fundamentally different from &Cell<T>.
edit:
On stable you can do this, but it doesn't work for unsized types (which should be fine because that's a rare use case):
use std::cell::Cell;
struct Person {
name: String,
age: usize,
}
impl Person {
fn age_nmut(this: &Cell<Self>) -> &Cell<usize> {
let this = this.as_ptr();
unsafe {
// this is safe because `&Cell` is not `Send`, so we have unique access to this `Cell`'s contents
// so we can temporarily create a reference to it's contents to get the field offset
// the address of a field is *never* less than the address of the type it came from
let this_addr = this as *const u8 as usize;
let age_addr = &(*this).age as *const usize as *const u8 as usize;
let offset = age_addr - this_addr;
// we have to use the original pointer because that has the right providence
&*this.cast::<u8>().add(offset).cast::<Cell<usize>>()
}
}
}
fn replace_first_with_last(party: &mut [Person]) {
let party = Cell::from_mut(party).as_slice_of_cells();
if let (Some(f), Some(l)) = (party.first(), party.last()) {
Person::age_nmut(f).set(Person::age_nmut(l).get());
}
}
I released a crate cell-project that exposes a safe interface for this pattern.
use std::cell::Cell;
use cell_project::cell_project as cp;
struct Person {
name: String,
age: usize,
}
fn replace_first_with_last(party: &mut [Person]) {
let party = Cell::from_mut(party).as_slice_of_cells();
if let (Some(f), Some(l)) = (party.first(), party.last()) {
cp!(Person, f.age).set(cp!(Person, l.age).get());
}
}
You can use extension traits to make this ergonomic
pub trait PersonExt {
fn age_nmut(&self) -> &Cell<usize>;
}
impl PersonExt for Cell<Person> {
fn age_nmut(&self) -> &Cell<usize> {
cp!(Person, self.age)
}
}
fn replace_first_with_last(party: &mut [Person]) {
let party = Cell::from_mut(party).as_slice_of_cells();
if let (Some(f), Some(l)) = (party.first(), party.last()) {
f.age_nmut().set(l.age_nmut().get());
}
}
I'm going to close this because, as was mentioned above,
adding a new reference type is a huge change
Lang absolutely expects that new things will be done with library types if at all possible -- see Pin, for example. Feel free to open something new if you have a situation that cannot be handled that way.
EDIT: Oops, thanks Steve!
(closing because this didn't actually get closed 8 days ago)
Most helpful comment
I released a crate
cell-projectthat exposes a safe interface for this pattern.You can use extension traits to make this ergonomic