Using the Rust compiler to prevent forgetting to call a method

I have a code like this:

foo.move_right_by(10); //do some stuff foo.move_left_by(10); 

It is very important that I perform both of these operations in the end, but I often forget to do the second after the first. This causes a lot of errors, and I am wondering if there is an idiomatic way of rust to avoid this problem. Is there a way to get the rust compiler to tell me when I forget?

My idea was to 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 do this?

Another idea is to implement Drop and panic! if the structure is discarded without calling this method. This is not so good though, because it is a runtime check, and it is highly undesirable.

Edit: I realized that my example might have been too simple. Logic can become 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(); 

Note how operations are not performed in a good, properly nested order. The only thing that matters is that the reverse operation is always called later. Sometimes the order must be defined in a certain way for the code to work properly.

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... 
+8
type-safety api-design rust
source share
3 answers

You can use phantom types to carry additional information that you can use to check types without incurring runtime costs. The 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 will not be a problem.

In addition, the compiler will complain if you are not actually using types in your structure, so you need to add fields that use them. Rust std provides a PhantomData type of zero size for convenience.

Your restriction can be encoded as follows:

 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 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 skip one of the required methods,

 fn main(){ let thing = Thing::new() .move_right_by(3); do_something(&thing); } 

then you will get a compilation 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>' 
+14
source share

I do not think that #[must_use] really what you want in this case. Here are two different approaches to solving your problem. The first is just to complete what you need to do in closure and divert 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 shell type that calls the function when it is deleted (similar to how Mutex::lock creates a MutexGuard , which unlocks Mutex when it is deleted):

 #[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); } 
+6
source share

I just looked at the initial description and maybe missed the details in the conversation, but one way to ensure that the actions are performed is to use the original object (going to the right) and replace it with one that will make you move the same amount to the left before you can do whatever you want to complete the task.

A new type may prohibit / require the execution of different calls before entering the ready 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 to the right, since MustGoLeft not allowed, but it can move to the left or open a window. If it moves far enough, it returns to CanGoRight state CanGoRight . But if he opens the window, then completely new rules apply. In any case, you will have to deal with both possibilities.

There will probably be some overlap between states, but should be simple enough for refactoring. Adding a custom tag can help.

In the end, it sounds like you are making a state machine. Perhaps https://hoverbear.org/2016/10/12/rust-state-machine-pattern/ will be useful.

+1
source share

All Articles