Crate apparat[][src]

Apparat

An ergomonic, flexible and light weight behavioral state machine

Notable features:

  • No-std compatible
  • Very small and fast to compile
    • Fewer than 250 lines of code
    • No dependencies
    • No procedural macros (just a declarative one)
  • Very ergonomic despite the manageable amount of macro magic
  • Highly flexible, suitable for many use cases
  • No dynamic dispatch, enables lots of compiler optimizations

Note: I am still experimenting with this a bit so while it’s below version 1.0, there might be breaking API changes in point releases. If you want to be sure, specify an exact version number in your Cargo.toml. In point-point-releases there won’t be breaking changes ever.

Architecture and usage

Types you provide

  • An arbitrary number of types that represent the different states*
  • A context type that holds data that shall be accessible in all states
  • An event type for the state machine to handle
  • An output type that is returned whenever an event is handled. If this is not needed, the unit type can be used.

* The data within the state types is exclusively accessible in the respective state. It get’s dropped on transition and should be rather small and cheap to move. If it’s not, consider putting it in a Box for better performance and memory efficiency.

Note: All provided types must implement the Debug trait.

Entities that are generated

  • A wrapper enum with variants for all provided state types
  • An implementation of the ApparatWrapper trait for the wrapper enum. This doesn’t provide any methods but only associates the other relevant types (Context, Event, Output) with the wrapper.
  • An implementation of the ApparatState trait for the wrapper enum. This way, the enum delegates all calls to methods of that trait to the current state. This is done without dynamic dispatch, just using match statements.
  • Implementations of the Wrap trait for all provided states (for convenience)

Traits

ApparatState<StateWrapper>

This trait must be implemented for all state types. The handle method is the only one that doesn’t provide a default impl and must be written manually. There are two other methods in the trait that form an initialization mechanism: After initializing the Apparat using the new methods or after handling any event, init is called on the new state, until its is_init method returns false. This way multiple transitions can be triggered by a single event. This happens in a while loop without stressing the recursion limit. If a state doesn’t need that initialization, the methods can be ignored so their default implementation is getting used.

The handle method returns a Handled<StateWrapper> struct where StateWrapper is the state wrapper enum that apparat generated for us. Handled<StateWrapper> just combines this enum with the provided output type. If this output type implements the Default trait, the StateWrapper can be turned into a Handled<StateWrapper> with the default output value using into(). This is demonstrated and commented in the example below.

TransitionFrom<OtherState, ContextData>

The TransitionFrom trait can be used to define specific transitions between states. The TransitionTo trait is then automatically implemented, just to be able to call the transition method using the turbofish syntax. This mechanism is similar to From and Into in the rust standard library. The difference to std::convert::From is that TransitionFrom can also mutate the provided context as a side effect. The usage of these traits is optional but recommended.

Wrap<StateWrapperType>

The Wrap<StateWrapper> trait provides a wrap method to turn an individual state into a StateWrapper. This is prefered over using into because it makes the code more readable and enables wrapper type inference in more cases. This trait is automatically implemented for all state types by the macro.

Minimal example

For a slightly more complete example, have a look at counter.rs in the examples directory.

use apparat::prelude::*;

// Define the necessary types
// --------------------------

// States

#[derive(Debug, Default)]
pub struct StateA;

#[derive(Debug, Default)]
pub struct StateB {
    events: usize, // just an example of a state holding exclusive data
}

// Context

// Data that survives state transitions and can be accessed in all states
#[derive(Debug, Default)]
pub struct ContextData {
    toggled: usize,
}

// Auto-generate the state wrapper and auto-implement traits
// ---------------------------------------------------------

// Since we are only handling one kind of event in this example and we don't
// care about values being returned when events are handled, we are just using
// the unit type for `event` and `output`
build_wrapper! {
    states: [StateA, StateB],
    wrapper: MyStateWrapper, // this is just an identifier we can pick
    context: ContextData,
    event: (),
    output: (),
}

// Define transitions
// ------------------

impl TransitionFrom<StateB> for StateA {
    fn transition_from(_prev: StateB, ctx: &mut ContextData) -> Self {
        println!("B -> A          | toggled: {}", ctx.toggled);
        StateA::default()
    }
}

impl TransitionFrom<StateA> for StateB {
    fn transition_from(_prev: StateA, ctx: &mut ContextData) -> Self {
        println!("A -> B          | toggled: {}", ctx.toggled);
        StateB::default()
    }
}

// Implement the `ApparateState` trait for all states
// --------------------------------------------------

impl ApparatState for StateA {
    type Wrapper = MyStateWrapper;

    fn handle(self, _event: (), ctx: &mut ContextData) -> Handled<MyStateWrapper> {
        println!("A handles event | toggled: {}", ctx.toggled);
        // increase toggled value
        ctx.toggled += 1;
        self.transition::<StateB>(ctx)
            .wrap() // turn the `StateB` into a `MyStateWrapper`
            .into() // turn it into a `Handled<MyStateWrapper>`
                    // Using `into` assumes you want to use the default value
                    // of your output type. It only works if your output type
                    // implements the `Default` trait in the first place.
    }
}

impl ApparatState for StateB {
    type Wrapper = MyStateWrapper;

    fn handle(mut self, _event: (), ctx: &mut ContextData) -> Handled<MyStateWrapper> {
        println!("B handles event | toggled: {}", ctx.toggled);
        self.events += 1;
        if self.events > 2 {
            self.transition::<StateA>(ctx).wrap().into()
        } else {
            self.wrap().into()
        }
    }
}

// Run the machine
// ---------------

fn main() {
    let mut apparat = Apparat::new(StateA::default().wrap(), ContextData::default());

    // Handle some events
    for _ in 0..10 {
        apparat.handle(());
    }
}

Modules

prelude

Macros

build_wrapper

Generate an enum that wraps all provided state types. Additionally all necessary traits are implemented for it, so the wrapper can be used within an Apparat state machine.

Structs

Apparat

The actual state machine that handles your events and manages their initialization and transitions

Handled

This type is being returned whenever an event is handled by a state type. It contains the new state alongside an output value that will be returned to the caller of the handle method.

Traits

ApparatState

A trait that must be implemented by all provided state types. Have a look at the readme or the examples for details.

ApparatTrait
ApparatWrapper

This trait is used to associate all the types used together in an Apparat with the state wrapper enum, so users of the library dont’ need to specify all these types every time they implement the ApparatState trait for one of their state types.

TransitionFrom

Define transitions between states. These transition functions can access the shared context/data mutably.

TransitionTo

Similar to Into in std, this is mostly for convenience and should not get implemented manually. Implement TransitionFrom instead and use TransitionInto in trait bounds, when needed.

Wrap

An alternative to std::Into for turning state types into the respective state wrapper enum. This is preferred over Into because it provides more reliable type inference in the context of apparat.