A zero-cost, effect-based state machine library for Rust.
Mindset provides a flexible and type-safe state machine implementation that separates pure guard logic from effectful actions. Built on Stillwater 0.11.0's effect system, it enables you to write state machines that are:
- Zero-cost by default: No runtime overhead when effects aren't needed
- Explicitly effectful: Side effects are opt-in and clearly marked
- Highly testable: Pure guards and dependency injection via environment traits
- Type-safe: Compile-time guarantees about state transitions
- Pure Guard Functions: Deterministic state validation with no side effects
- Effectful Actions: Explicit I/O and side effects when needed
- Environment Pattern: Clean dependency injection for testing
- Zero-Cost Abstractions: Pay only for what you use
- Composable Effects: Build complex behavior from simple trait combinations
use mindset::{StateMachine, State};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum ConnectionState {
Disconnected,
Connecting,
Connected,
}
impl State for ConnectionState {}
fn main() {
use mindset::builder::{StateMachineBuilder, TransitionBuilder};
let machine = StateMachineBuilder::new()
.initial(ConnectionState::Disconnected)
.add_transition(
TransitionBuilder::new()
.from(ConnectionState::Disconnected)
.to(ConnectionState::Connecting)
.succeeds()
.build()
.unwrap()
)
.add_transition(
TransitionBuilder::new()
.from(ConnectionState::Connecting)
.to(ConnectionState::Connected)
.succeeds()
.build()
.unwrap()
)
.build()
.unwrap();
// Zero-cost transitions - compiles to direct state updates
}use mindset::{StateMachine, State};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum OrderState {
Draft,
Submitted,
Processing,
Completed,
}
impl State for OrderState {}
struct Order {
id: u64,
total: f64,
}
// Define environment capabilities as traits
trait PaymentProcessor {
fn charge(&mut self, amount: f64) -> Result<(), String>;
}
trait Logger {
fn log(&mut self, message: &str);
}
// Pure guard - no side effects
fn can_submit(order: &Order) -> bool {
order.total > 0.0
}
// Effectful action - explicit environment usage
fn submit_order<Env>(order: &mut Order, env: &mut Env) -> Result<(), String>
where
Env: PaymentProcessor + Logger,
{
env.log(&format!("Submitting order {}", order.id));
env.charge(order.total)?;
Ok(())
}
fn main() {
use mindset::builder::{StateMachineBuilder, TransitionBuilder};
let machine = StateMachineBuilder::new()
.initial(OrderState::Draft)
.add_transition(
TransitionBuilder::new()
.from(OrderState::Draft)
.to(OrderState::Submitted)
.succeeds()
.build()
.unwrap()
)
.add_transition(
TransitionBuilder::new()
.from(OrderState::Submitted)
.to(OrderState::Processing)
.succeeds()
.build()
.unwrap()
)
.build()
.unwrap();
// For custom effectful actions, see the Builder Guide
}-
Pure Guards, Effectful Actions
- Guards are pure functions that validate state transitions
- Actions perform side effects and state changes
- Clear separation enables testing and reasoning
-
Zero-Cost by Default
- No effects means no runtime overhead
- Effects are opt-in via explicit environment parameters
- Compiler optimizes away unused abstractions
-
Environment Pattern
- Dependencies expressed as trait bounds
- Compose environments from multiple traits
- Easy mocking for tests
-
Explicit Over Implicit
- Side effects are visible in function signatures
- No hidden global state or implicit context
- Clear data flow
A state machine in Mindset consists of:
- States: Enum variants representing discrete system states
- Transitions: Allowed moves between states
- Guards: Pure functions determining if transition is allowed
- Actions: Effectful functions executed during transition
- Environment: External dependencies and services
Transitions come in two flavors:
machine.transition(State::A, State::B, |state| {
// Pure guard logic
state.is_valid()
});Compiles to direct state updates with no runtime overhead.
machine.transition_with_effect(State::A, State::B, |state, env| {
// Guard check
if !state.is_valid() {
return Err("Invalid state");
}
// Effects
env.log("Transitioning");
env.save_to_db(state)?;
// State update
state.version += 1;
Ok(())
});Effects are explicit via environment parameter. Only pay for what you use.
Run any example with cargo run --example <name>:
| Example | Demonstrates |
|---|---|
| basic_state_machine | Zero-cost state machine with pure transitions |
| effectful_state_machine | Environment pattern and effectful actions |
| testing_patterns | Testing with mock environments |
| traffic_light | Simple cyclic state machine |
| document_workflow | Multi-stage approval workflow |
| order_processing | E-commerce order lifecycle |
| account_management | Account states with validation |
| checkpoint_resume | Checkpoint and resume patterns |
| mapreduce_workflow | MapReduce workflow implementation |
| resource_management | Resource lifecycle management |
See examples/ directory for full code and examples/README.md for detailed explanations.
use mindset::state_enum;
use mindset::builder::{StateMachineBuilder, simple_transition};
state_enum! {
enum TrafficLight {
Red,
Yellow,
Green,
}
}
let machine = StateMachineBuilder::new()
.initial(TrafficLight::Red)
.transitions(vec![
simple_transition(TrafficLight::Red, TrafficLight::Green),
simple_transition(TrafficLight::Green, TrafficLight::Yellow),
simple_transition(TrafficLight::Yellow, TrafficLight::Red),
])
.build()
.unwrap();use mindset::state_enum;
use mindset::builder::{StateMachineBuilder, TransitionBuilder};
state_enum! {
enum DocState {
Draft,
Review,
Approved,
Published,
}
final: [Published]
}
trait AuditLog {
fn log_transition(&mut self, from: DocState, to: DocState);
}
let machine = StateMachineBuilder::new()
.initial(DocState::Draft)
.add_transition(
TransitionBuilder::new()
.from(DocState::Draft)
.to(DocState::Review)
.succeeds()
.build()
.unwrap()
)
.add_transition(
TransitionBuilder::new()
.from(DocState::Review)
.to(DocState::Approved)
.succeeds()
.build()
.unwrap()
)
.build()
.unwrap();struct Account {
balance: f64,
status: AccountStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum AccountStatus {
Active,
Suspended,
Closed,
}
impl State for AccountStatus {}
trait AccountRepository {
fn persist(&mut self, account: &Account) -> Result<(), String>;
}
fn can_close(account: &Account) -> bool {
account.balance == 0.0
}
fn close_account<Env>(account: &mut Account, env: &mut Env) -> Result<(), String>
where
Env: AccountRepository,
{
if !can_close(account) {
return Err("Cannot close account with non-zero balance".to_string());
}
account.status = AccountStatus::Closed;
env.persist(account)?;
Ok(())
}The environment pattern makes testing straightforward:
#[cfg(test)]
mod tests {
use super::*;
struct MockEnv {
logged: Vec<String>,
saved: Vec<String>,
}
impl Logger for MockEnv {
fn log(&mut self, msg: &str) {
self.logged.push(msg.to_string());
}
}
impl Database for MockEnv {
fn save(&mut self, data: &str) -> Result<(), String> {
self.saved.push(data.to_string());
Ok(())
}
}
#[test]
fn test_effectful_transition() {
let mut state = State::Initial;
let mut env = MockEnv {
logged: vec![],
saved: vec![],
};
transition(&mut state, &mut env).unwrap();
assert_eq!(env.logged.len(), 1);
assert_eq!(env.saved.len(), 1);
}
}Pure transitions have zero runtime overhead. This code:
machine.transition(State::A, State::B);Compiles to the same assembly as:
state = State::B;Effects only cost what you use:
- No effects: Zero overhead (direct state update)
- Single trait: Monomorphized static dispatch (zero-cost abstraction)
- Multiple traits: One vtable lookup per trait (if using trait objects)
- Environment mutation: Direct field access
On a typical modern CPU:
- Pure transition: ~0.5ns (equivalent to direct assignment)
- Single-effect transition: ~2-3ns (includes function call overhead)
- Multi-effect transition: ~5-10ns (depends on effect complexity)
Mindset includes built-in checkpoint and resume functionality for long-running MapReduce workflows. This allows workflows to be paused and resumed without losing progress, making the system resilient to interruptions.
- Automatic Checkpointing: Workflows automatically save progress at key phases (map completion, reduce rounds)
- Serialization Formats: Support for both JSON (human-readable) and binary (compact) formats
- Atomic Writes: Checkpoint writes use atomic file operations to prevent corruption
- Resume from Interruption: Seamlessly continue workflows after crashes, stops, or planned maintenance
Long-running workflows benefit from checkpointing when:
- Processing large datasets that take hours or days
- Running on infrastructure that may experience interruptions
- Needing to pause workflows during high-demand periods
- Debugging or inspecting intermediate workflow state
- Optimizing costs by pausing during expensive compute periods
# Start a workflow
./workflow run --config workflow.yaml
# Workflow saves checkpoints automatically...
# Interrupt with Ctrl+C or system failure
# Resume from checkpoint
./workflow resume --checkpoint ./checkpoints/latest.json
# Workflow continues from where it left offFor detailed documentation on checkpoint structure, resume behavior, best practices, and atomic write patterns, see the Checkpointing Guide.
- Builder Guide: Comprehensive guide to the builder API with examples and patterns
- Checkpointing Guide: Checkpoint and resume for long-running workflows
- Effects Guide: Comprehensive guide to effect patterns
- API Documentation: Generated API docs
- Pure core: Business logic and guards are pure functions
- Effectful shell: I/O and side effects at the boundaries
- Clear separation enables testing and reasoning
- Zero-cost when effects aren't needed
- Explicit opt-in for effects
- No hidden overhead or runtime costs
- Side effects visible in function signatures
- Environment dependencies declared as trait bounds
- No magic or hidden behavior
- Pure functions are trivial to test
- Mock environments for integration testing
- Clear dependency injection
This library implements the effect-based state machine foundation as specified in:
- Spec 001: Core state machine with pure guards
- Spec 002: Effect-based transitions with environment pattern
- Spec 004: Checkpoint and resume for persistence
- Spec 005: Builder API for ergonomic construction
Contributions are welcome! Please ensure:
- All tests pass
- Code follows project conventions
- Documentation is updated
- Commit messages are clear and descriptive
[License information to be added]