16

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...
4
  • 3
    Just to clarify: both methods have to be called at some point within the enclosing function? Is the order relevant? Or do you just want to make sure that, if move_right() is called, move_left() is called, too? Also: can you describe the enclosing function? Does it return anything? Commented Feb 4, 2017 at 9:00
  • OP, if you have to call move_left() after move_right() was called, you could bundle these functionalities to return a custom type (kind of a handler) that automatically calls move_left() on Drop - though that might not be possible if your second fn has to return values and so on ... It might be an idea, but it might get ugly rather quickly. Commented Feb 4, 2017 at 13:02
  • 1
    If you can express the serie of calls to be performed as a state machine, then states can be encoded as types and transitions as method calls on self (which consume the current state and produce a new one). Would it be possible, or is not flexible enough? Commented Feb 4, 2017 at 15:14
  • @LukasKalbertodt It's the second one: if you move right, you have to move left afterwards. It can't be a Drop like what musicmatze suggested because that would rely on the compiler to drop the value at the right time. We need more control than that. The enclosing function would be other operations that also move right and then move left by the same amount. It's important to always eventually return to the reference by moving left again. Commented Feb 4, 2017 at 19:45

3 Answers 3

20

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>`
Sign up to request clarification or add additional context in comments.

1 Comment

That's an interesting approach! Great way to get the compiler involved to make sure that it happens.
6

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);
}

4 Comments

Thanks for your answer! Unfortunately, the closure method doesn't work well because it needs to be a bit more flexible than that. I added another example in the question to clarify. The second approach is interesting, but what if I want to explicitly perform the inverse operation explicitly? It's hard to rely on the compiler to drop in time.
@SunjayVarma in Rust, because of how ownership works, dropping should be fully deterministic; also you could wrap your original Foo in some type that has the drop() method, and you could make sure this wrapper object's ownership goes out of scope at the right time. To me, it feels like the drop-based solution is probably the right way to go.
I agree that dropping is the right way. If you really need the drop to happen before the end of a scope, you can also just call en empty function which takes the ownership of the value, like here: gist.github.com/silmeth/f1d66c819862b418fd8862e3dc36875a – but when you forget to do it manually, the drop will happen eventually at the end of a scope, so you won’t miss calling the move_right() method.
When I said "it's hard to rely on the compiler to drop in time", I meant that sometimes I'll need to drop the value explicitly. While dropping is completely deterministic, it usually happens at the end of the scope so while that accomplishes the goal of calling the method eventually, it doesn't allow me to explicitly see when the method was called. Thank you for your responses this discussion is very interesting!
1

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.

1 Comment

This is similar to my answer, but actually a bit simpler. I think there are a few similar approaches, and you could probably even create a macro that could generate the type-level states and transitions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.