state_machine_future
Easily create type-safe Futures from state machines — without the boilerplate.
state_machine_future type checks state machines and their state transitions,
and then generates Future implementations and typestate0
boilerplate for you.
Introduction
Most of the time, using Future combinators like map and then are a great
way to describe an asynchronous computation. Other times, the most natural way
to describe the process at hand is a state machine.
When writing state machines in Rust, we want to leverage the type system to
enforce that only valid state transitions may occur. To do that, we want
typestates0: types that represents each state in the state
machine, and methods whose signatures only permit valid state transitions. But
we also need an enum of every possible state, so we can treat the whole
state machine as a single entity, and implement Future for it. But this is
getting to be a lot of boilerplate...
Enter #[derive(StateMachineFuture)].
With #[derive(StateMachineFuture)], we describe the states and the possible
transitions between them, and then the custom derive generates:
-
A typestate for each state in the state machine.
-
A type for the whole state machine that implements
Future. -
A concrete
startmethod that constructs the state machineFuturefor you, initialized to its start state. -
A state transition polling trait, with a
poll_zee_choomethod for each non-final stateZeeChoo. This trait describes the state machine's valid transitions, and its methods are called byFuture::poll.
Then, all we need to do is implement the generated state transition polling trait.
Additionally, #[derive(StateMachineFuture)] will statically prevent against
some footguns that can arise when writing state machines:
-
Every state is reachable from the start state: there are no useless states.
-
There are no states which cannot reach a final state. These states would otherwise lead to infinite loops.
-
All state transitions are valid. Attempting to make an invalid state transition fails to type check, thanks to the generated typestates.
Guide
Describe the state machine's states with an enum and add
#[derive(StateMachineFuture)] to it:
There must be one start state, which is the initial state upon construction;
one ready state, which corresponds to Future::Item; and one error
state, which corresponds to Future::Error.
Any other variants of the enum are intermediate states.
We define which state-to-state transitions are valid with
#[state_machine_future(transitions(...))]. This attribute annotates a state
variant, and lists which other states can be transitioned to immediately after
this state.
A final state (either ready or error) must be reachable from every intermediate state and the start state. Final states are not allowed to have transitions.
From this state machine description, the custom derive generates boilerplate for us.
For each state, the custom derive creates:
- A typestate for the state. The type's name matches the variant name, for
example the
Intermediatestate variant's typestate is also namedIntermediate. The kind of struct type generated matches the variant kind: a unit-style variant results in a unit struct, a tuple-style variant results in a tuple struct, and a struct-style variant results in a normal struct with fields.
State enum Variant |
Generated Typestate |
|---|---|
enum StateMachine { MyState, ... } |
struct MyState; |
enum StateMachine { MyState(bool, usize), ... } |
struct MyState(bool, usize); |
enum StateMachine { MyState { x: usize }, ... } |
struct MyState { x: usize }; |
- An
enumfor the possible states that can come after this state. Thisenumis namedAfterXwhereXis the state's name. There is also aFrom<Y>implementation for eachYstate that can be transitioned to afterX. For example, theIntermediatestate would get:
Next, for the state machine as a whole, the custom derive generates:
-
A state machine
Futuretype, which is essentially anenumof all the different typestates. This type is namedBlahFuturewhereBlahis the name of the state machine descriptionenum. In this example, where the state machine description is namedMyStateMachine, the generated state machine future type would be namedMyStateMachineFuture. -
A polling trait,
PollBordlewhereBordleis this state machine description's name. For each non-final stateTootWasabi, this trait has a method,poll_toot_wasabi, which is likeFuture::pollbut specialized to the current state. Each method takes conditional ownership of its state (viaRentToOwn) and returns afutures::Poll<AfterThisState, Error>whereErroris the state machine's error type. This signature does not allow invalid state transitions, which makes attempting an illegal state transition fail to type check. Here is theMyStateMachine's polling trait, for example:
-
An implementation of
Futurefor that type. This implementation dispatches to the appropriate polling trait method depending on what state the future is in:-
If the
Futureis in theStartstate, then it uses<MyStateMachine as PollMyStateMachine>::poll_start. -
If it is in the
Intermediatestate, then it uses<MyStateMachine as PollMyStateMachine>::poll_intermediate. -
Etc...
-
-
A concrete
startmethod for the description type (soMyStateMachine::startin this example) which constructs a new state machineFuturetype in its start state for you. This method has a parameter for each field in the start state variant.
Start enum Variant |
Generated start Method |
|---|---|
MyStart, |
fn start() -> MyStateMachineFuture { ... } |
MyStart(bool, usize), |
fn start(arg0: bool, arg1: usize) -> MyStateMachineFuture { ... } |
MyStart { x: char, y: bool }, |
fn start(x: char, y: bool) -> MyStateMachineFuture { ... } |
Given all those generated types and traits, all we have to do is impl PollBlah for Blah for our state machine Blah.
Context
The state machine also allows to pass in a context that is available in every poll_* method
without having to explicitly include it in every one.
The context can be specified through the context argument of the state_machine_future attribute.
This will add parameters to the start method as well as to each poll_* method of the trait.
extern crate state_machine_future;
extern crate futures;
use *;
use *;
Same as for the state argument, the context can be taken through the RentToOwn type!
However, be aware that once you take the context, the state machine will always return
Async::NotReady without invoking the poll_ methods anymore.
That's it!
Example
Here is an example of a simple turn-based game played by two players over HTTP.
extern crate state_machine_future;
extern crate futures;
use ;
use RentToOwn;
/// The result of a game.
/// Some kind of simple turn based game.
///
/// ```text
/// Invite
/// |
/// |
/// | accept invitation
/// |
/// |
/// V
/// WaitingForTurn --------+
/// | ^ |
/// | | | receive turn
/// | | |
/// | +-------------+
/// game concludes |
/// |
/// |
/// |
/// V
/// Finished
/// ```
// Now, we implement the generated state transition polling trait for our state
// machine description type.
// To spawn a new `Game` as a `Future` on whatever executor we're using (for
// example `tokio`), we use `Game::start` to construct the `Future` in its start
// state and then pass it to the executor.
Attributes
This is a list of all of the attributes used by state_machine_future:
-
#[derive(StateMachineFuture)]: Placed on anenumthat describes a state machine. -
#[state_machine_future(derive(Clone, Debug, ...))]: Placed on theenumthat describes the state machine. This attribute describes which#[derive(...)]s to place on the generatedFuturetype. -
#[state_machine_future(start)]: Used on a variant of the state machine descriptionenum. There must be exactly one variant with this attribute. This describes the initial starting state. The generatedstartmethod has a parameter for each field in this variant. -
#[state_machine_future(ready)]: Used on a variant of the state machine descriptionenum. There must be exactly one variant with this attribute. It must be a tuple-style variant with one field, for exampleReady(MyItemType). The generatedFutureimplementation uses the field's type asFuture::Item. -
#[state_machine_future(error)]: Used on a variant of the state machine descriptionenum. There must be exactly one variant with this attribute. It must be a tuple-style variant with one field, for exampleError(MyError). The generatedFutureimplementation uses the field's type asFuture::Error. -
#[state_machine_future(transitions(OtherState, AnotherState, ...))]: Used on a variant of the state machine descriptionenum. Describes the states that this one can transition to.
Macro
An auxiliary macro is provided that helps reducing boilerplate code for state transitions. So, the following code:
Ok(Ready(NextState(1).into()))
Can be reduced to:
transition!(NextState(1))
Features
Here are the cargo features that you can enable:
debug_code_generation: Prints the code generated by#[derive(StateMachineFuture)]tostdoutfor debugging purposes.
License
Licensed under either of
at your option.
Contribution
See CONTRIBUTING.md for hacking.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

