Closed ryan-summers closed 5 months ago
Just ran into same problem. Your proposed change would be very beneficial.
I had the same issue, but I simulated this by having an intermediate state and an event queue/channel.
A naive example:
use anyhow::{bail, Result};
use rand::RngCore;
use std::collections::VecDeque;
smlang::statemachine! {
transitions: {
*Idle + Start / start_something = Starting,
Starting + StartSucceeded / start_succeeded = Started,
Starting + StartFailed(anyhow::Error) / start_failed = Idle,
}
}
struct Context {
events: VecDeque<Events>,
}
impl Context {
fn new() -> Self {
Context {
events: VecDeque::new(),
}
}
}
impl StateMachineContext for Context {
fn start_something(&mut self) {
match randomly_fails() {
Ok(()) => self.events.push_back(Events::StartSucceeded),
Err(e) => self.events.push_back(Events::StartFailed(e)),
}
}
fn start_succeeded(&mut self) {
println!("Yay!");
}
fn start_failed(&mut self, error: &anyhow::Error) {
println!("Error: {error}");
}
}
impl StateMachine<Context> {
fn process_events(&mut self, ev: Events) -> Result<&States, Error> {
self.context_mut().events.push_back(ev);
while let Some(ev) = self.context_mut().events.pop_front() {
self.process_event(ev)?;
}
Ok(self.state())
}
}
fn randomly_fails() -> Result<()> {
if rand::thread_rng().next_u32() % 2 == 0 {
Ok(())
} else {
bail!("Oh no!");
}
}
fn main() {
let mut sm = StateMachine::new(Context::new());
// Notice that I use `process_events` instead of `process_event`.
// I know it's a bad name.
let state = sm.process_events(Events::Start).unwrap();
match state {
States::Idle => println!("new state: Idle"),
States::Started => println!("new state: Started"),
_ => unreachable!(),
}
}
Also in my opinion, if we consider the action to be a transition to the next state and if our action fails at the half of the process, that means we need to restore some of the state back. My example above will force you to implement this correctly. If the action was fallible, most probably mistakes will happen.
Just a recomendation since my PR (#49) affects this:
If you plan to add this feature, one way to provide a way to the user to have owned values, is to pass to the actions a &mut
values. With this way, a user can have an Option<T>
as state data, and then use .take()
to get the owned value.
Right now, actions are only used to always return data associated with the new state (or no data).
However, in real-world state machines, an action is often fallible (e.g. needs to return a
Result
).Currently, with a fallible action, you have to perform the action in a
guard
. This is undesirable because:It seems like it may be most beneficial to have actions return a
Result<NextStateData, Error>
to accomodate this use case.