Looking at some old toy code I wrote in C# a couple of years ago I noticed a pattern I immediately knew wouldn't work in Rust because of the borrow checker. A minimal Rust equivalent would look like this:
struct Foo { /* ... */ }
impl Foo {
fn mutate(&mut self) { /* ... */ }
fn with_user_code<F>(&mut self, mut user_code: F) where F: FnMut() {
self.mutate();
user_code();
self.mutate();
}
}
fn main() {
let mut foo = Foo{};
foo.mutate();
foo.with_user_code(|| foo.mutate()); // error because of double mut borrow
}
Because of the exclusivity rules for mutable references it won't work. As far as I can see, the options are to either
- use c-style function pointers and pass in an extra
&mut Foo
parameter on each usage, which is boilerplate-y as I basically reimplement closures manually just to please the borrow checker
- implement it using macros (which I haven't learnt yet, and it seems to be overkill to use them just for the sake of getting rid of a borrowing error)
Given that this usage should be perfectly safe (after all, either the with_user_code
method runs, or the passed user_code
closure, but not both at the same time), there should be a way to do this.
Or maybe I'm missing something else here, is there some other idiomatic Rust pattern to solve this problem?
why do you need a "c-style" function pointer? just don't capture the object in the closure, but use an argument instead:
-fn with_user_code<F>(&mut self, mut user_code: F) where F: FnMut() {
+fn with_user_code<F>(&mut self, mut user_code: F) where F: FnMut(&mut Self) {
self.mutate();
- user_code();
+ user_code(self);
self.mutate();
}
fn main() {
let mut foo = Foo{};
foo.mutate();
- foo.with_user_code(|| foo.mutate());
+ foo.with_user_code(|foo| foo.mutate());
}
2 Likes
That's pretty much what I imagined but I guess I worded it kinda confusingly. What I meant was that this approach is pretty much equivalent to a plain c function pointer, you don't capture anything, but instead pass in the environment explicitly. This works, but you now need the extra foo argument in the function passed into with_user_code
every single time, exactly the thing closures were invented to make more ergonomic by capturing their dependencies.
that's not true, you cannot capture values borrowing the Foo
, but other states can still be captured.
one thing to understand is, closures (non-move
closures, that is) starts borrowing the captured environment when it is constructed, NOT when it is invoked. to state it in another way, closure captures are NOT syntactic or symbolic, closures don't capture identities, they capture values.
in rust, a value should be borrowed only during the short period when you use it, you should NOT borrow some value "as a syntactical shorthand", or "just to save it for later convenience".
for people coming from strong OO background, it is important to shift your mindset from "objects" to "values".
an object typically had an identity, and it is usually reified as the memory address of the object in most languages. but values don't have identifies, and many optimizations in rust relies on the ability to move values all over the places, and part of the borrow checker's job is to ensure these moves are safe.
for example, a common mistake by new learners is trying to return a value from a function together with a reference to it. returning a value means to move the value (from the callee to the caller), but borrowed values just cannot be moved.
2 Likes