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 |
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 |
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 |
TransitionFrom | Define transitions between states. These transition functions can access the shared context/data mutably. |
TransitionTo | Similar to |
Wrap | An alternative to |