Hi rust forum!
I am having some trouble combining a function that takes and object and returns the same object, with other code that takes this object as a mutable reference. I created a small piece of code to demonstrate this (playground):
use std::mem;
struct MyNum(u32);
struct Point {
x: MyNum,
}
fn calc(num: MyNum) -> MyNum {
MyNum(num.0 + 1)
}
/*
/// This will not compile.
fn change_point1(point: &mut Point) {
point.x = calc(point.x); // Cannot move out of borrowed content
}
*/
/// mem::replace approach
/// Drawback: There is the danger of forgetting to set point.x
/// in some flow of the function.
fn change_point2(point: &mut Point) {
let x = mem::replace(&mut point.x, MyNum(0));
// What if we forget to do this in some flow:
point.x = calc(x);
}
/// Take ownership of the point, and return a new point.
/// Drawback: This function has output.
/// If we wanted to return a result, this function will have
/// output of (Point, Result<...>) which is not fun to use.
fn change_point3(point: Point) -> Point {
Point {
x: calc(point.x),
}
}
fn main() {
let mut point = Point { x: MyNum(0) };
change_point2(&mut point);
let point = change_point3(point);
assert_eq!(point.x.0, 2);
}
We have some internal type, MyNum
, and a function calc
that takes a MyNum
and produces a new one.
Then there is some outer structure, Point, that contains a field x of type MyNum
. We want to apply the function calc
. Assume that we can not change the signature of calc
: It must take ownership over the argument.
I consider three approaches to modifying the internal field x.
change_point1, which I hoped would work, is by doing this: point.x = calc(point.x)
. This does not compile, with the compiler error "Cannot move out of borrowed content".
I have the feeling that something is wrong with doing this, but I'm not sure what. My guess is that the compiler is afraid calc
will "eat" the field x
in case of a panic!, and then point
will not have a valid state.
change_point2 uses the mem::replace
. I have seen this trick in many places inside Tokio's futures code, mostly with state machines.
mem::replace
will replace the field x
with some dummy temporary field, giving ownership over the previous value. Using this method we can have point.x = calc(x)
and it compiles.
The main drawbacks of this method are that we need to come up with some dummy instance, which is somewhat strange/ugly. In addition, it is possible to forget to set back point.x = calc(x)
, and then you are left with the dummy object at point.x
. It is hard to see how one can forget to set point.x
in the example above, but it could happen if your function gets bigger. This happened to me more than once, and it took me a while to spot it.
change_point3 is more functional / immutable approach. This reminds me of things I have seen with redux in javascript. Instead of taking &mut Point
I am taking Point
as an argument and return a Point.
Using this method I do not need to come up with a dummy object, and I also don't have the danger of forgetting to set point.x. However, I get a function with a more complicated signature.
If I also want to return a Result from the function (In case the function calc
could fail), the resulting function signature will be something like:
fn change_point3(point: Point) -> (Point, Result<(),SomeError>)
which is not very comfortable to use.
My current approach is using change_point3
when I can, because I feel like it is the safest. However, there are cases when this approach is not possible. For example, when doing a Future implementation, one has to implement a poll() function that has this signature:
fn poll(&mut self) -> Poll<Self::Item, Self::Error>
If I want to implement Future
for the Point
object from above, I will have to use the approach of change_point2
, as follows (Note: I did not try to compile this, but it should compile):
impl Future for Point {
type Item = ();
type Error = ();
fn poll(&mut self) -> Poll<(), ()> {
let x = mem::replace(&mut self.x, MyNum(0));
self.x = calc(x);
Ok(Async::Ready(())
}
}
In this case I can not use change_point3
approach, because the signature of poll
forces me to take a &mut Point.
My question is this: I would like to know if there is a sane way to make change_point1
work, or if you know of a better way to solve this issue.