Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forbid partial outbound moves from immutable variables #238

Closed
wants to merge 12 commits into from

Conversation

CloudiDust
Copy link
Contributor

This RFC proposes that the semantics of immutable variables be refined by forbidding partial outbound moves, so immutable variables in Rust become more immutable.

2. guaranteed lifetimes for values with move semantics ("movable values") can be achieved by combining this with explicit drop calls.

This makes NoisyDrop/QuietDrop from #210 unnecessary.

(EDIT: Guaranteed lifetimes can be provided by explicit drop calls alone.)

Rendered View.

@CloudiDust
Copy link
Contributor Author

cc @pnkfelix @rkjnsn.


Rust's "immutable" variables do *not* provide *strict immutability*. There are three exceptions:

1. it is legal to have internal mutability even inside "immutable" variables, via `UnsafeSell<T>`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnsafeSell should be UnsafeCell.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zwarich Thanks, corrected.


```rust
#[deriving(Show)]
enum Gender { Male, Female }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. Even for 'sex' instead of 'gender' it'd be inaccurate.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to be pedantic SJWs about this? Its an example, its not a social statement.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to pedantically defend this choice? It's an example, no need to make a social statement by keeping it precisely like this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust' score values include inclusivity. Please refer to the Code of Conduct.

Beyond that, this is just factually wrong. Intersex people exist.

Furthermore, it is trivial to provide another example that isn't exclusionary.=

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be pedantic: it's not factually wrong if the domain of discussion is e.g. the ends of certain I/O cables. (At least, I'm not aware of a new in vogue term there, though I am out of touch with current A/V terminology).

But otherwise I agree: it's trivial to change the example, and I would rather discussion not be further derailed.

Can we just assume for now that the author will adjust it in the near future?


(Update: of course the context of this example is not A/V cabling but rather a struct Person, so it is not like @steveklabnik 's comment was unwarranted. I was just idly musing.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would just change it to any other enumeration. The Django people use a conference talk example of submitted/reviewed/accepted/rejected, but it's really not as important, as long as it's not gender/sex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really stupid nitpick, I can't believe I'm wasting my time reading this garbage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iopq https://github.com/rust-lang/rust/wiki/Note-development-policy#conduct

Please review the first, third, sixth and eighth bullets. Please also review the "Moderation" section beneath the code of conduct, notably about "avoid flirting with offensive or sensitive issues, particularly if they're off-topic"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'll note that @CloudiDust had a particularly good response to this issue; it might not be something they've really thought about before, but, once the issue was raised, they were clearly very happy to make the small amount of effort it takes be more inclusive. 👏 )

@netvl
Copy link

netvl commented Sep 13, 2014

Why do you call outbound move a mutation? For example, I do not see it as a mutation, because the values themselves are not modified (they are just shallow-copied), and moving immutable values around is a very natural concept to me.

@CloudiDust
Copy link
Contributor Author

@netvl, moves are mutations, they at least mutate the bindings between variables and values. Semantically, moves are not shallow copies but, eh, moves. If moves were shallow copies, then the original variables/fields should still contain values after moves, but actually they will contain nothing.

Full outbound moves only mutate bindings, so they can be seen as "not mutations" in the sense that the values themselves are not mutated.
But partial ones would also mutate the parent values, so I don't think calling them "immutable" can be justified.

@Gankra
Copy link
Contributor

Gankra commented Sep 13, 2014

@netvl Well, strictly speaking, with the current move semantics it totally is a real mutation: http://is.gd/3ru22U

That said, I find being able to move out of immutable values to be handy, and the only way to "expose" any mutation is with unsafe code. Anything else will just trigger a compiler error for trying to access the partially or totally moved value.

I find the overall premise of the RFC confusing, though. It says that removing partial moves from immutable values removes the need for NoisyDrop, but I don't see how that follows. As I understood it, the point of NoisyDrop is that a destructor might be re-prioritized if there exists a branch that moves the value before the end of the function, and a branch that doesn't. This seems to be a problem regardless of partial moves, and this solution doesn't even address the issue of partial moves if I have the value mutably. Am I missing something?

@CloudiDust
Copy link
Contributor Author

s/"immutable"/"not mutations", sorry cannot edit on the phone.

@CloudiDust
Copy link
Contributor Author

@gankro, NoisyDrop and QuietDrop don't completely solve the real problem IMHO. The real problem is how to guarantee that a value has a certain lifetime. And this problem exists regardless of which drop semantics we use.

The change in the RFC by itself doesn't enable guaranteed lifetimes. But combining this with explicit drops will do. And explicit drops need no RFC as they are already supported by the language, just not used to enforce this guarantee cuurrently.

@CloudiDust
Copy link
Contributor Author

@gankro, this RFC only forbids partial outbound moves. And I agree that this move pattern can be handy sometimes. (Please refer to the Drawbacks section.)

My full solution to the ergonomic issues caused by this RFC is "fine grained mutation control with the pin or pinned keyword", which among other mutation patterns, supports partial moves out of immutables if needed, but that is a more involved feature and is intended for post-1.0 Rust.

@huonw
Copy link
Member

huonw commented Sep 14, 2014

Note that non-mut variables are not really immutable, e.g. let x = Cell::new(1); x.set(2);.

@sinistersnare
Copy link

+1 this or Alternative 1, partial moves are a bit confusing and it would be better to use the ref pattern more often.

@CloudiDust
Copy link
Contributor Author

@huonw, yes and I listed this as one of the exceptions in the RFC.

This exception is justified because Cells are explicitly designed for internal mutability, a exceptional case, and the programmer must opt-in.

While partial outbound moves are mutations that the programmer can not opt-out currently.

@sfackler
Copy link
Member

You can't move out of types with destructors.

@CloudiDust
Copy link
Contributor Author

@sfackler, thanks and I'll add discussions about types with Drop into the RFC soon.

@netvl
Copy link

netvl commented Sep 14, 2014

@gankro, wow, that's surprising. However, I think that since this can only be observed via unsafe code, and the original binding is prohibited from future use, it is acceptable behavior for me.

@CloudiDust, well, modifying bindings is not the same as modifying values, and I'd argue that mut only is related to modifying values. You can modify bindings with multiple lets with the same variable, and in the process you can even change mutability of the value.

Anyway, now I see the argument why partial moves are mutations. However, this is very special kind of mutation. It is handled differently by the compiler, it causes different errors, it is natural (for me, at least), the fact that the binding is mutated can't be observed in the safe code (same as in the case of full move) and I'd argue that it is not against intuition. Even if the field is actually mutated, it still can't be used in safe code.

How is really this:

struct X { a: Vec<int>, b: Vec<int> }

let x = X { a: vec![], b: vec![] };

different from this:

let a = vec![];
let b = vec![];

? Moving from a or b in either case does not affect the other value which can still be used, and even if x in the first case can't be used, well, that's very natural.

The other argument (related to drop) still holds, I guess, but I'm not that familiar with drop-related proposals to say anything about that.

@CloudiDust
Copy link
Contributor Author

@netvl, in the first case, the value in x changed its state from "usable as a whole" to "unusable as a whole". I'd argue that this change shouldn't happen when the value is still in an immutable variable.

But a programmer can explicitly move the whole value to a mutable variable first, and then do value mutation on it. And this is fine.

In the second case, no value is mutated, only bindings are.

As you have pointed out, mut is related to mutating values, so I think it is natural for mut to control all value mutations. Thus it should control partial outbound moves too, which means mutable and immutable variables should differ on whether they allow partial outbound moves.

@pnkfelix
Copy link
Member

@CloudiDust in my opinion, your RFC's description of "Exception 1" is misrepresenting the situation. @huonw is pointing out in his comment that in addition to UnsafeCell, the libraries also offer Cell and RefCell, which have no mention of the word "unsafe". (There are also all of the thread-safe atomic types in std::sync::atomic, which again require no mut annotation on their definition site.)

An insight that I had while preparing my ML workshop talk was that mut is not about controlling all mutation. It is merely about controlling operations which require exclusive access.

That is, writing let x = ...; let mut y = ...; is not saying that the state of x is never mutated. It is merely saying that x is never mutated in a manner that requires exclusive access for soundness and data-race freedom; likewise, the let mut y = ... is merely signalling that some operation on a potential control flow path will request exclusive access to y.


I realize that this comment is not providing much direct feedback on this RFC itself (apart from undermining the proposed model of immutability that the RFC is trying to shoehorn onto Rust). I am not yet convinced that the change proposed here would buy us all that much, but I need to think on the matter more.

@nrc
Copy link
Member

nrc commented Sep 14, 2014

@pnkfelix:

An insight that I had while preparing my ML workshop talk was that mut is not about controlling all mutation. It is merely about controlling operations which require exclusive access.

As Niko said a while back 'mut' really means unique, 'insight' is exactly how it felt when he pointed that out. But lets not restart the mutpocalypse :-)

Sorry for off-topic.

@pnkfelix
Copy link
Member

@nick29581 yes, my "insight" was really just finding a nuanced phrase to make it not sound like we made a grave mistake in our own terminology + UX. :)

@CloudiDust
Copy link
Contributor Author

@pnkfelix @nick29581

Thanks for the new perspective. Yeah I remember the mutpocalypse as well.

But the problem is that we are not using uniq and &uniq, but mut and &mut. And it can be argued that in practice, uniqueness and aliasing control are means to an end. They are implementation details for Rust's mutation control semantics, so it is fine for the mutation control semantics to differ somewhat from "pure" aliasing control semantics.

Or we can expose uniq and &uniq instead, and tell the users that "if mutation control is needed, use aliasing control", but this seems a bit far fetched at first glance.

So my preference would be sacrificing a bit of theoretical beauty and do what seems practically more "correct".

@CloudiDust
Copy link
Contributor Author

@nick29581 @pnkfelix And I will adjust the RFC accordingly. Guess I have much to change tonight. ;)

@CloudiDust
Copy link
Contributor Author

Everyone:

As @rkjnsn pointed out in the comments in #210, partial moves themselves forbid full moves later.

So explicit drop alone can guarantee lifetimes. Which eliminates half the usefulness of this RFC.

Now the problem becomes: Should mut fit programmer intuition more or fit its theoretical foundation more? Is the change worthwhile?

@CloudiDust
Copy link
Contributor Author

@nick29581 @pnkfelix, I think from the exclusive access/uniqueness/aliasing control point of view, the semantics of a "bare variable" (that is one without mut) is "this variable contains a value that you cannot (statically) request exclusive access of", and it says nothing about allowing or forbidding partial outbound moves. So I guess it will also be fine to add the restriction then?

@bill-myers
Copy link

No, the value is not mutated, since you can't use the parts that are moved out (of course, moving something back in is and must be disallowed).

Disallowing this seems fundamentally wrong, since it means that "let (a, b) = (Foo::new(), Foo::new());" and "let p = (Foo::new(), Foo::new());", which intuitively should be fully eqivalent, are no longer equivalent, since you can move out a and b, but not the two parts of p

You could forbid all moving of non-mut let variables, but this is also fundamentally wrong, because dropping a value is an implicit move, and if dropping is possible (which it must be), then it must be possible to consume the value otherwise, which requires moving.

Or in other words, if you don't use a part of the variable, who cares what happened to it, and if you use it, it either has the initial value or the program doesn't compile, so it's fine.

@netvl
Copy link

netvl commented Sep 15, 2014

@CloudiDust,

in the first case, the value in x changed its state from "usable as a whole" to "unusable as a whole". I'd argue that this change shouldn't happen when the value is still in an immutable variable.

Well, I disagree. I don't see why immutability should disallow this. Looks like we have different notions of what is "natural" here :)

I think it is natural for mut to control all value mutations. Thus it should control partial outbound moves too, which means mutable and immutable variables should differ on whether they allow partial outbound moves.

Again, while moves, partial or not, do mutate values, it is not observable, because the compiler will never allow you to use or overwrite the moved value through the affected binding. Consequently, the immutable value is semantically immutable even in presence of partial moves.

@CloudiDust
Copy link
Contributor Author

@netvl @bill-myers, yeah, we have different notions of what is "natural" here.

I have realized, both perspective are internally consistent in their own ways.

My perspective is: immutable values should never be mutated in any way, only bindings can, so full outbound moves are fine, while partial outbound moves should be forbidden.

While you two's perspective is: immutable variables and fields should follow the same rule: they either have the initial value, or are unusable, so partial outbound moves are fine, just like full outbound moves are.

So this RFC has no clear advantage in the semantics aspect, and also no clear advantage in the bug catching aspect. (In practice, the bug in the example snippet would most likely be caught eventually, just not when the move happened, but when we try to use person or person.name later.)

Thus I believe the benefits of this RFC to be negligible, and not worth the cost in ergonomics. I'll withdraw this RFC.

Thanks everyone. :)

EDIT: Made the descriptions of the two perspectives clearer.

@CloudiDust CloudiDust closed this Sep 15, 2014
@CloudiDust
Copy link
Contributor Author

Also @steveklabnik, I think some material here may be worth incorporating into the Guide or other discussions about mut somewhere in the Rust doc. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.