State machine in rust?

Viewed 85

In the C family of languages I usually implement a state machine as a series of if else statements and enums, where if statements check in which state the machine is and the bodies execute state transitions, for example this:

if(current_left_state == GLFW_PRESS && !left_pressed)
{
    left_pressed = true;
    return MouseInputState::LEFT_DOWN;
}
if(current_left_state == GLFW_PRESS && left_pressed)
{
    left_pressed = true;
    return MouseInputState::LEFT_DRAG;
}
if(current_left_state == GLFW_RELEASE && left_pressed)
{
    left_pressed = false;
    return MouseInputState::LEFT_UP;
}
if(current_right_state == GLFW_PRESS && !right_pressed)
{
    right_pressed = true;
    return MouseInputState::RIGHT_DOWN;
}

Rust has a lot of idiomatic sugar which is nice, I was wondering if there is a way to use rust's syntactic sugar to make cleaner state machines.

Like, there has got to be a better way than this:

MouseState::NoAction =>
{
    if *button == glfw::MouseButtonLeft && *action == glfw::Action::Press
    {
        return MouseState::LeftDown;
    }
    if *button == glfw::MouseButtonRight && *action == glfw::Action::Press
    {
        return MouseState::RightDown;
    }
    return MouseState::NoAction;
}
MouseState::LeftDown =>
{
    if *button == glfw::MouseButtonLeft && *action == glfw::Action::Release
    {
        return MouseState::LeftUp;
    }
    return MouseState::LeftDrag;
}
MouseState::LeftDrag =>
{
    if *button == glfw::MouseButtonLeft && *action == glfw::Action::Release
    {
        return MouseState::LeftUp;
    }
    return MouseState::LeftDrag;
}
MouseState::LeftUp =>
{
    if *button == glfw::MouseButtonLeft && *action == glfw::Action::Press
    {
        return MouseState::LeftUp;
    }
    return MouseState::NoAction;
}
2 Answers

For really clean state machines, you can match on tuples:

let next_state = match (current_state, button, action) => {
    (MouseState::NoAction, glfw::MouseButtonLeft, glfw::Action::Press) => MouseState::LeftDown,
    (MouseState::NoAction, glfw::MouseButtonRight, glfw::Action::Press) => MouseState::RightDown,
    (MouseState::NoAction, _, _) => MouseState::RightDown,
    (MouseState::LeftDown, glfw::MouseButtonLeft, glfw::Action::Release) => MouseState::LeftUp,
    (MouseState::LeftDown, _, _) => MouseState::LeftDrag,
    (MouseState::LeftDrag, glfw::MouseButtonLeft, glfw::Action::Release) => MouseState::LeftUp,
    (MouseState::LeftDrag, _, _) => MouseState::LeftDrag,
    (MouseState::LeftUp, glfw::MouseButtonLeft, glfw::Action::Press) => MouseState::LeftUp,
    (MouseState::LeftUp, _, _) => MouseState::NoAction,
    _ => MouseState::NoAction,
}

Depending on your preference, you can consolidate the branches with the same result using the | pattern.

I personally love using functional style with Option type, it leads to very concise code, although unlike if statements it may check branches that are known to be false.

To use it, you convert boolean typed conditions into Option, and combine them via and, or, map and a plethora of other functions available in Option type

Playground link

#[derive(Debug)]
enum MouseState {
    NoAction, LeftDown, LeftDrag, LeftUp, RightDown
}

fn main() {
    let mut state = MouseState::NoAction;

    let left = (*button == glfw::MouseButtonLeft).then_some(());
    let right = (*button == glfw::MouseButtonRight).then_some(());
    let press = (*action == glfw::Action::Release).then_some(());
    let release = (*action == glfw::Action::Press).then_some(());
    
    // just to test
    //let left = Some(());
    //let right: Option<()> = None;
    //let press = Some(());
    //let release: Option<()> = None;
    
    state = match state {
        MouseState::NoAction =>
        {
            press
                .and(left.map(|_| MouseState::LeftDown))
                .or(right.map(|_| MouseState::RightDown))
                .unwrap_or(MouseState::NoAction)
        }
        MouseState::LeftDown | MouseState::LeftDrag =>
        {
            release
                .and(left)
                .map(|_| MouseState::LeftUp)
                .unwrap_or(MouseState::LeftDrag)
        }
        MouseState::LeftUp =>
        {
            press
                .and(left)
                .map(|_| MouseState::LeftUp)
                .unwrap_or(MouseState::NoAction)
        }
        x => x, // other states are left unchanged
    };
    println!("{:?}", state);
}
Related