Sad Machine

sad_machine provides a macro to declaratively define a state machine and the transitions between states. It's focused on providing a nice API for applications that deal with event loops and use a state machine to keep track of their state.

sad_machine is a fork of the sm library which removes the traits and keeps only the macro, and redesigns the generated code to be more enum-friendly.

Usage

sad_machine exposes only one macro, state_machine!. A quick example:

```rust use sadmachine::statemachine;

state_machine! { Lock { InitialStates { Locked, Unlocked }

    TurnKey {
        Locked => Unlocked
        Unlocked => Locked
    }

    BreakKeyhole {
        Locked, Unlocked => Broken
    }

    Repair {
        Broken => Locked
    }
}

}

fn main() { let mut lock = Lock::locked();

loop {
    match lock {
        Lock::Locked(m @ LockedState::FromInit) => lock = m.turn_key(),
        Lock::Unlocked(m) => lock = m.turn_key(),
        Lock::Locked(m) => lock = m.break_keyhole(),
        Lock::Broken(_) => break,
    }
}

assert_eq!(lock, Lock::Broken(BrokenState::FromBreakKeyhole));

} ```

In this example, the macro generated:

A few differences from sm's API:

Descriptive Example

The below example explains step-by-step how to create a new state machine using the provided macro, and then how to use the created machine in your code.

Declaring a new State Machine

First, we import the macro from the crate:

rust use sad_machine::state_machine;

Next, we initiate the macro declaration:

rust state_machine! {

Then, provide a name for the machine, and declare a list of allowed initial states:

rust Lock { InitialStates { Locked, Unlocked }

Finally, we declare one or more events and the associated transitions:

```rust TurnKey { Locked => Unlocked Unlocked => Locked }

    BreakKeyhole {
        Locked, Unlocked => Broken
    }
}

} ```

And we're done. We've defined our state machine structure, and the valid transitions, and can now use this state machine in our code.

Using your State Machine

You can initialise the machine as follows:

rust let sm = Lock::locked();

We've initialised our machine in the Locked state. The sm is as an enum covering all possible states of the state machine, and each state contains the name of the event that triggered it. A full pattern match on the state enum looks like this:

rust match lock { Lock::Locked(LockedState::FromInit) => .., Lock::Locked(LockedState::FromTurnKey) => .., Lock::Locked(LockedState::FromRepair) => .., Lock::Unlocked(UnlockedState::FromInit) => .., Lock::Unlocked(UnlockedState::FromTurnKey) => .., Lock::Broken(BrokenState::FromBreakKeyhole) => .., }

To transition this machine to the Unlocked state, we send the turn_key method on the LockedState object:

rust let lock = match lock { Lock::Locked(locked) => locked.turn_key(), _ => panic!("wrong state"), }

Caveat emptor

The state machine does not consume the previous state when performing a transition, as opposed to sm's behavior, so be careful when operating in a concurrent context.

It also doesn't prevent you from constructing a state that is not one of the initial states, due to Rust's lack of private constructors for enums.

Why fork

Some of the design choices that sm makes conflict with my use case.

I was using the library in an event loop where:

  1. The state is stored as its enum Variant representation, and can only advance by one step in a single loop
  2. Multiple events can trigger a state change to a certain state, but I don't particularly care about the event that triggered the stage change

sm seems to have different design goals:

  1. The transition method returns a Machine type and not an enum, which forces me to call .as_enum() on its result every time to store it as Variant, but makes it easy to trigger multiple state transitions in a single piece of code
  2. The cases of the Variant enum also include the name of the event that triggered the state change, which led me to duplicate code in multiple branches for each state that had multiple entry points

sad_machine's API focuses on the state enum rather than on concrete states. The transition methods it generates return the enum, which makes it harder to trigger multiple transitions in the same piece of code, but on the other hand it removes the cruft of calling .as_enum() on the result, and its state enum does not encode the event name in the name of its cases, but rather carries it inside itself.

This forks keeps sm's parser for the DSL to define the state machine and changes the generated code.

License

Licensed under either of

This was the license of the original crate and I'd rather not change it.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.