I have some code like this:
foo.move_right_by(10);
//do some stuff
foo.move_left_by(10);
It's really important that I perform both of those operations eventually, but I often forget to do the second one after the first. It causes a lot of bugs and I'm wondering if there is an idiomatic Rust way to avoid this problem. Is there a way to get the rust compiler to let me know when I forget?
My idea was to maybe somehow have something like this:
// must_use will prevent us from forgetting this if it is returned by a function
#[must_use]
pub struct MustGoLeft {
steps: usize;
}
impl MustGoLeft {
fn move(&self, foo: &mut Foo) {
foo.move_left_by(self.steps);
}
}
// If we don't use left, we'll get a warning about an unused variable
let left = foo.move_left_by(10);
// Downside: move() can be called multiple times which is still a bug
// Downside: left is still available after this call, it would be nice if it could be dropped when move is called
left.move();
Is there a better way to accomplish this?
Another idea is to implement Drop
and panic!
if the struct is dropped without having called that method. This isn't as good though because it's a runtime check and that is highly undesirable.
Edit: I realized my example may have been too simple. The logic involved can get quite complex. For example, we have something like this:
foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.close_box();
Notice how the operations aren't performed in a nice, properly nested order. The only thing that's important is that the inverse operation is always called afterwards. The order sometimes needs to be specified in a certain way in order to make the code work as expected.
We can even have something like this:
foo.move_right_by(10);
foo.open_box(); // like a cardboard box, nothing to do with Box<T>
foo.move_left_by(10);
// do more stuff...
foo.move_right_by(10);
foo.close_box();
foo.move_left_by(10);
// do more stuff...
You can use phantom types to carry around additional information, which can be used for type checking without any runtime cost. A limitation is that move_left_by
and move_right_by
must return a new owned object because they need to change the type, but often this won't be a problem.
Additionally, the compiler will complain if you don't actually use the types in your struct, so you have to add fields that use them. Rust's std
provides the zero-sized PhantomData
type as a convenience for this purpose.
Your constraint could be encoded like this:
use std::marker::PhantomData;
pub struct GoneLeft;
pub struct GoneRight;
pub type Completed = (GoneLeft, GoneRight);
pub struct Thing<S = ((), ())> {
pub position: i32,
phantom: PhantomData<S>,
}
// private to control how Thing can be constructed
fn new_thing<S>(position: i32) -> Thing<S> {
Thing {
position: position,
phantom: PhantomData,
}
}
impl Thing {
pub fn new() -> Thing {
new_thing(0)
}
}
impl<L, R> Thing<(L, R)> {
pub fn move_left_by(self, by: i32) -> Thing<(GoneLeft, R)> {
new_thing(self.position - by)
}
pub fn move_right_by(self, by: i32) -> Thing<(L, GoneRight)> {
new_thing(self.position + by)
}
}
You can use it like this:
// This function can only be called if both move_right_by and move_left_by
// have been called on Thing already
fn do_something(thing: &Thing<Completed>) {
println!("It's gone both ways: {:?}", thing.position);
}
fn main() {
let thing = Thing::new()
.move_right_by(4)
.move_left_by(1);
do_something(&thing);
}
And if you miss one of the required methods,
fn main(){
let thing = Thing::new()
.move_right_by(3);
do_something(&thing);
}
then you'll get a compile error:
error[E0308]: mismatched types
--> <anon>:49:18
|
49 | do_something(&thing);
| ^^^^^^ expected struct `GoneLeft`, found ()
|
= note: expected type `&Thing<GoneLeft, GoneRight>`
= note: found type `&Thing<(), GoneRight>`
I don't think #[must_use]
is really what you want in this case. Here's two different approaches to solving your problem. The first one is to just wrap up what you need to do in a closure, and abstract away the direct calls:
#[derive(Debug)]
pub struct Foo {
x: isize,
y: isize,
}
impl Foo {
pub fn new(x: isize, y: isize) -> Foo {
Foo { x: x, y: y }
}
fn move_left_by(&mut self, steps: isize) {
self.x -= steps;
}
fn move_right_by(&mut self, steps: isize) {
self.x += steps;
}
pub fn do_while_right<F>(&mut self, steps: isize, f: F)
where F: FnOnce(&mut Self)
{
self.move_right_by(steps);
f(self);
self.move_left_by(steps);
}
}
fn main() {
let mut x = Foo::new(0, 0);
println!("{:?}", x);
x.do_while_right(10, |foo| {
println!("{:?}", foo);
});
println!("{:?}", x);
}
The second approach is to create a wrapper type which calls the function when dropped (similar to how Mutex::lock
produces a MutexGuard
which unlocks the Mutex
when dropped):
#[derive(Debug)]
pub struct Foo {
x: isize,
y: isize,
}
impl Foo {
fn new(x: isize, y: isize) -> Foo {
Foo { x: x, y: y }
}
fn move_left_by(&mut self, steps: isize) {
self.x -= steps;
}
fn move_right_by(&mut self, steps: isize) {
self.x += steps;
}
pub fn returning_move_right(&mut self, x: isize) -> MovedFoo {
self.move_right_by(x);
MovedFoo {
inner: self,
move_x: x,
move_y: 0,
}
}
}
#[derive(Debug)]
pub struct MovedFoo<'a> {
inner: &'a mut Foo,
move_x: isize,
move_y: isize,
}
impl<'a> Drop for MovedFoo<'a> {
fn drop(&mut self) {
self.inner.move_left_by(self.move_x);
}
}
fn main() {
let mut x = Foo::new(0, 0);
println!("{:?}", x);
{
let wrapped = x.returning_move_right(5);
println!("{:?}", wrapped);
}
println!("{:?}", x);
}
I only looked at the initial description and probably missed the details in the conversation but one way to enforce the actions is to consume the original object (going right) and replace it with one that forces you to to move left by same amount before you can do whatever you wanted to do to finish the task.
The new type can forbid / require different calls to be made before getting to a finished state. For example (untested):
struct CanGoRight { .. }
impl CanGoRight {
fn move_right_by(self, steps: usize) -> MustGoLeft {
// Note: self is consumed and only `MustGoLeft` methods are allowed
MustGoLeft{steps: steps}
}
}
struct MustGoLeft {
steps: usize;
}
impl MustGoLeft {
fn move_left_by(self, steps: usize) -> Result<CanGoRight, MustGoLeft> {
// Totally making this up as I go here...
// If you haven't moved left at least the same amount of steps,
// you must move a bit further to the left; otherwise you must
// switch back to `CanGoRight` again
if steps < self.steps {
Err(MustGoLeft{ steps: self.steps - steps })
} else {
Ok(CanGoRight{ steps: steps - self.steps })
}
}
fn open_box(self) -> MustGoLeftCanCloseBox {..}
}
let foo = foo.move_right_by(10); // can't move right anymore
At this point foo
can no longer move right as it isn't allowed by MustGoLeft
but it can move left or open the box. If it moves left far enough it gets back to the CanGoRight
state again. But if it opens the box then totally new rules apply. Either way you'll have to deal with both possibilities.
There's probably going to be some duplication between the states, but should be easy enough to refactor. Adding a custom trait might help.
In the end it sounds like you're making a state machine of sorts. Maybe https://hoverbear.org/2016/10/12/rust-state-machine-pattern/ will be of use.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With