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>'