Expand description
The asynchronous hierarchical state machine (HSM)
This state machine is an programmatic approach. It uses features of async-await functionality, and combines it with usage of the process stack and scoping.
Instead of storing state as variable, the state is represented by an async function. The transition from one state to new state is a sequence of async function invocations, either within a single ‘Composite’ or lifting back to the parent ‘Composite’, and so on.
The following state machine diagram is implemented by the following code, entering the menu and couting the amount of ping-pongs.
@startuml res/hierarchy
[*] --> App
state App {
[*] --> Menu
state Menu {
}
state Play {
[*] --> Ping
Ping --> Ping : ping
Ping --> Pong : ping
Pong --> Pong : pong
Pong --> Ping : ping
}
Menu --> Play: play
Play --> Menu: menu
}
App --> [*]: terminate
@enduml
See here the ping pong state diagram
use async_std::prelude::*;
use async_std::stream;
use async_std::task;
use async_hsm::{Composite, Transit, Builder, BuilderPair};
use std::rc::Rc;
use std::cell::RefCell;
type Score = u32;
type EnterStateScore = u32;
type AppComposite = Composite<AppData>;
type PlayComposite = Composite<AppData>;
type AppTransit<'s> = Transit<'s, AppComposite, Score, AppError>;
type PlayTransit<'s> = Transit<'s, PlayComposite, BuilderPair<AppComposite, EnterStateScore, Score, AppError>, AppError>;
type AppBuilder = Builder<AppComposite, EnterStateScore, Score, AppError>;
type AppBuilderPair = BuilderPair<AppComposite, EnterStateScore, Score, AppError>;
#[derive(Debug, Clone, PartialEq)]
enum AppError { Failure }
static TO_MENU: AppBuilder = || |comp, score| Box::pin(menu(comp, score));
static TO_PLAY: AppBuilder = || |comp, score| Box::pin(play(comp, score));
static TERMINATE: AppBuilder = || |comp, score| Box::pin(terminate(comp, score));
#[derive(Debug, Clone, PartialEq)]
enum IoEvent { Ping, Pong, Terminate, Menu, Play }
#[derive(Debug, Clone)]
struct AppData {
event: Rc<RefCell<stream::FromIter<std::vec::IntoIter<IoEvent>>>>,
}
async fn pong<'s>(comp: &'s mut PlayComposite, score: Score) -> Result<PlayTransit<'s>, AppError> {
let mut score = score + 1;
let event = comp.data.event.clone();
while let Some(event) = (*event).borrow_mut().next().await {
match event {
IoEvent::Ping => return Ok(Transit::To(Box::pin(ping(comp, score)))),
IoEvent::Terminate => return Ok(Transit::Lift((TERMINATE, score))),
_ => score += 1,
}
}
Ok(Transit::Lift((TERMINATE, score)))
}
async fn ping<'s>(comp: &'s mut PlayComposite, score: Score) -> Result<PlayTransit<'s>, AppError> {
let mut score = score + 1;
let event = comp.data.event.clone();
while let Some(event) = (*event).borrow_mut().next().await {
match event {
IoEvent::Pong => return Ok(Transit::To(Box::pin(pong(comp, score)))),
IoEvent::Terminate => return Ok(Transit::Lift((TERMINATE, score))),
_ => score += 1,
}
}
Ok(Transit::Lift((TERMINATE, score)))
}
async fn terminate<'s>(_comp: &'s mut AppComposite, score: Score) -> Result<AppTransit<'s>, AppError> {
Ok(Transit::Lift(score))
}
async fn play<'s>(comp: &'s mut AppComposite, score: Score) -> Result<AppTransit<'s>, AppError> {
let event = comp.data.event.clone();
let mut play = PlayComposite::new(AppData { event: event });
let (builder, build_arg): AppBuilderPair = play.init(ping, score).await?;
builder()(comp, build_arg).await
}
async fn menu<'s>(comp: &'s mut AppComposite, score: Score) -> Result<AppTransit<'s>, AppError> {
let score = score;
let event = comp.data.event.clone();
while let Some(event) = (*event).borrow_mut().next().await {
match event {
IoEvent::Play => return Ok(Transit::To(Box::pin(play(comp, score)))),
IoEvent::Terminate => return Ok(Transit::Lift(score)),
_ => continue,
}
}
Ok(Transit::Lift(score))
}
#[test]
fn test_game() {
let sequence = vec![IoEvent::Play, IoEvent::Ping, IoEvent::Pong,
IoEvent::Ping, IoEvent::Pong, IoEvent::Terminate];
let event = Rc::new(RefCell::new(stream::from_iter(sequence)));
let start_score = 0;
let mut app = AppComposite::new(AppData { event: event });
let result: Result<Score, AppError> = task::block_on(app.init(menu, start_score));
assert_eq!(Ok(5), result);
}