3
\$\begingroup\$

While learning Rust for the past couple of weeks, I set out to use the type-system to build a static State Machine implementation in Rust.

The goals:

  • program in idiomatic Rust
  • no runtime overhead
  • use traits for flexibility
  • optionally use macro's for ease of use
  • keep it simple

I believe I managed to achieve all, except for maybe the third point. The end-result is a macro that generates a new struct for each state machine, so there's no overarching trait to easily generate your own state machine without the included macro.

I'm curious:

  • If I approached this in the right way
  • What I could have done differently
  • If I wanted to rely more heavily on the trait system (while keeping the zero-runtime cost) how I would go about doing that

My next extension to this would be to implement some kind of enum-based type on top of this, to make it easy to match against the current state, as right now that's not yet possible.

Code can be viewed on GitHub, or below:

#[macro_export]
macro_rules! sm {
    (
        $($name:ident {
            States { $($state:ident),+ $(,)* }

            $($event:ident {
                $($($from:ident),+ => $to:ident)+
            })*
        })+
    ) => {
        use $crate::{AsEnum, Machine as M, Transition};

        $(
            #[allow(non_snake_case)]
            pub mod $name {
                use $crate::{AsEnum, Event, Machine as M, State, Transition};

                #[derive(PartialEq, Eq, Debug)]
                pub struct Machine<S: State>(pub S);

                impl<S> M for Machine<S> where S: State {
                    type State = S;

                    fn state(&self) -> S {
                        self.0.clone()
                    }
                }

                impl<S> Machine<S> where S: State {
                    pub fn new(state: S) -> Self {
                        Machine(state)
                    }
                }

                $(
                    #[derive(Copy, Clone, Eq, Debug)]
                    pub struct $state;
                    impl State for $state {}

                    impl PartialEq<$state> for $state {
                        fn eq(&self, _: & $state) -> bool {
                            true
                        }
                    }
                )*

                #[derive(Debug)]
                pub enum States {
                    $($state(Machine<$state>)),*
                }

                $(
                    impl AsEnum<$state> for Machine<$state> {
                        type Enum = States;

                        fn as_enum(self) -> Self::Enum {
                            States::$state(self)
                        }
                    }
                )*

                sm!{@recurse ($($state),*), ()}

                $(
                    #[derive(PartialEq, Eq, Debug)]
                    pub struct $event;
                    impl Event for $event {}

                    $(
                        $(
                            impl Transition<Machine<$to>, $event> for Machine<$from> {
                                fn transition(self, _: $event) -> Machine<$to> {
                                    Machine::new($to)
                                }
                            }
                        )*
                    )*
                )*
            }
        )*
    };

    (@recurse ($state:ident, $($other:ident),+), ($($old:ident),*)) => {
        $(
            impl PartialEq<$other> for $state {
                fn eq(&self, _: & $other) -> bool {
                    false
                }
            }
        )*

        $(
            impl PartialEq<$old> for $state {
                fn eq(&self, _: & $old) -> bool {
                    false
                }
            }
        )*

        sm!{@recurse ($($other),*), ($($old,)* $state)}
    };

    (@recurse ($state:ident), ($($old:ident),*)) => {
        $(
            impl PartialEq<$old> for $state {
                fn eq(&self, _: & $old) -> bool {
                    false
                }
            }
        )*
    };
}
\$\endgroup\$
  • \$\begingroup\$ I just realised that while all my tests are green, that's because they only test the "happy path". You can't actually do any conditional expression on the current state, since its type differs (by design) from any of the other states, and so a derived PartialEq won't work. I guess the solution(?) to this is to somehow recurse over all other states for each state, and generate a PartialEq<OtherState> implementation for each, which returns false. I do feel like this is where I'm probably lacking insights into the proper use of generics to not have to do this recursion for every state. \$\endgroup\$ – JeanMertz Sep 4 '18 at 13:03
  • \$\begingroup\$ I've since solved the above mentioned problem and updated both the URL and the inline code example. Feel free to leave me any feedback! \$\endgroup\$ – JeanMertz Sep 4 '18 at 21:00
  • \$\begingroup\$ I've updated the code again to the latest iteration, in case anyone is interested in giving it a review. I'm also in the process of rewriting it to use procedural macros, but that's quite a change in code, so perhaps a new review would be best, instead of updating this again. Still very much interested in learning from anyone that has some interesting insights to share! \$\endgroup\$ – JeanMertz Sep 12 '18 at 19:38
  • \$\begingroup\$ Why use macros? State machines can be implemented in Rust with the type system alone. This uses the concept of typestates and is likely much easier to reason about. rust-embedded.github.io/book/static-guarantees/… \$\endgroup\$ – RubberDuck Jan 7 at 1:14
  • 1
    \$\begingroup\$ I see now. Maybe some examples of using the generated implementations would help. I guess you could consider that a review in and of itself. The macros do reduce duplication and theoretically maintenance cost, but it’s much harder to reason about, which increases the maintenance cost. It’s probably a wash at the end of the day. \$\endgroup\$ – RubberDuck Jan 10 at 21:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.