Partial moves are fun! One thing to note is that methods which contain a partial move may not compile because the analysis doesn’t introspect beyond function boundaries. So even if the function body only uses, say, self.0, the overall function moves self, and so a partial move that would be acceptable without a function boundary becomes unacceptable with a function boundary in the presence of another partial move in the calling context.
Good article! I’m grappling with a similar problem lately. Coming from OO background, I tend to think about functions with a &self argument. However a fn with &mut self, Rust borrow checker doesn’t “see past” the fn. If I work directly with the fields of a struct, I can have multiple partial &mut borrows of different parts of the struct. It means I am forced to break encapsulation to make my code compile. Here’s an example of what I mean.
struct Foo {
p: u8,
q: u8,
}
impl Foo {
fn borrow_p(&mut self) -> &mut u8 {
&mut self.p
}
}
fn main() {
let mut f = Foo { p: 1, q: 2 };
let fp = &mut f.p; // works
// let fp = f.borrow_p(); // doesn't work
let fq = &mut f.q;
*fp = 3;
*fq = 4;
}
If the field doesn’t implement Copy, then “using the field directly” would mean moving it, which would mean either borrowing or moving the containing type.
Then you could just mutably borrow, which is what you’d be doing by returning a mutable reference from a method. If you didn’t look at the context of my question, I’m asking what benefit f.borrow_p() would have over &mut f.p.
Generally, if code is split out into a function, I’d expect it to be either for the purpose of reuse / encapsulation of some sequence of operations, or for stylistic / readability purposes. It may be frustrating then, when trying to reuse some code, or make your code appear more consistent with your personal taste or the styleguide of a project or team, to find that the code is rejected by the compiler because the function boundary obscures the disjointness of a borrow or partiality of a move.
I think you might just be running into an artifact of a simplistic example meant to demonstrate the problem. The example doesn’t motivate the problem though, because the example has a very simple work-around.
cc @algesten - This is indeed one of my biggest complaints with Rust as a language. There is a palpable tension between the borrow checker and decomposition.
I don’t think so. Because that would imply the implementation of a function would leak into its signature somehow. The only things I’ve seen to address this are adding a way to explicitly annotate what a routine does and doesn’t borrow. But those are just brainstorming ideas. I don’t think there are any concrete plans to address this yet.
Gotcha, and if you pick out items from one of the Vecs inside a method and return a mutable reference it prevents you from doing anything with the other one? I can see why that could cause problems.
It took me a second to get what you meant by “Personally, I don’t see why Rust prevents moves like this, since it could easily reason that z is only partially defined in the same way that it already does for p”… it turns out there’s an issue that seems relevant: https://github.com/rust-lang/rust/issues/54987 it doesn’t touch on the half-owned bit, but it probably relates to why you can’t at this point accept the assignment to z even if only z.1 is touched later on. Interesting post.
Short and sweet and pretty helpful. Thanks :)
Glad you liked it!
Partial moves are fun! One thing to note is that methods which contain a partial move may not compile because the analysis doesn’t introspect beyond function boundaries. So even if the function body only uses, say,
self.0
, the overall function movesself
, and so a partial move that would be acceptable without a function boundary becomes unacceptable with a function boundary in the presence of another partial move in the calling context.This same problem exists with disjoint borrows.
Good article! I’m grappling with a similar problem lately. Coming from OO background, I tend to think about functions with a
&self
argument. However afn
with&mut self
, Rust borrow checker doesn’t “see past” thefn
. If I work directly with the fields of astruct
, I can have multiple partial&mut
borrows of different parts of the struct. It means I am forced to break encapsulation to make my code compile. Here’s an example of what I mean.What’s the benefit of using a mutable reference to a field over just using the field directly?
If the field doesn’t implement
Copy
, then “using the field directly” would mean moving it, which would mean either borrowing or moving the containing type.Then you could just mutably borrow, which is what you’d be doing by returning a mutable reference from a method. If you didn’t look at the context of my question, I’m asking what benefit
f.borrow_p()
would have over&mut f.p
.Generally, if code is split out into a function, I’d expect it to be either for the purpose of reuse / encapsulation of some sequence of operations, or for stylistic / readability purposes. It may be frustrating then, when trying to reuse some code, or make your code appear more consistent with your personal taste or the styleguide of a project or team, to find that the code is rejected by the compiler because the function boundary obscures the disjointness of a borrow or partiality of a move.
I think you might just be running into an artifact of a simplistic example meant to demonstrate the problem. The example doesn’t motivate the problem though, because the example has a very simple work-around.
But it should be easy to see how this can become a problem in practice when things get a tiny bit more complicated. I documented one such example of it happening here (along with my work-around): https://github.com/BurntSushi/regex-automata/blob/6682f9cef5b4ef0c0892c4a6589658cd171106d8/src/nfa/compiler.rs#L22-L32
cc @algesten - This is indeed one of my biggest complaints with Rust as a language. There is a palpable tension between the borrow checker and decomposition.
Thank you, this is the kind of answer I was hoping for.
Yes. My problem is more complex than the example (multiple
Vec
where I want one element of each as it were).I wonder if any of the future work on Polonius/NLL and forward will be able to “see through” at least the simplest of these cases.
I don’t think so. Because that would imply the implementation of a function would leak into its signature somehow. The only things I’ve seen to address this are adding a way to explicitly annotate what a routine does and doesn’t borrow. But those are just brainstorming ideas. I don’t think there are any concrete plans to address this yet.
Adding on, I think “borrow analysis won’t introspect into functions” has been a pretty solid position for the Rust project for years at this point.
I’m thinking that NLL kinda magically put invisible lifetimes inside a scope that you only discover once you break them.
Partially borrowing a struct “through” the fn barrier could be a similar problem. It wouldn’t show in the signature, but be a compiler problem.
Like. The signature says
&'a self
, but in practice that could mean some part ofself
.It’s a simplified example. What I have in practice is two
Vec
where I pick out specific items inside ones to be mutated.Gotcha, and if you pick out items from one of the
Vec
s inside a method and return a mutable reference it prevents you from doing anything with the other one? I can see why that could cause problems.It took me a second to get what you meant by “Personally, I don’t see why Rust prevents moves like this, since it could easily reason that z is only partially defined in the same way that it already does for p”… it turns out there’s an issue that seems relevant: https://github.com/rust-lang/rust/issues/54987 it doesn’t touch on the half-owned bit, but it probably relates to why you can’t at this point accept the assignment to z even if only z.1 is touched later on. Interesting post.