• Stars
    star
    322
  • Rank 130,398 (Top 3 %)
  • Language
    Rust
  • Created almost 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Easily create type-safe `Future`s from state machines β€” without the boilerplate.

state_machine_future

Build Status

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 start method that constructs the state machine Future for you, initialized to its start state.

  • A state transition polling trait, with a poll_zee_choo method for each non-final state ZeeChoo. This trait describes the state machine's valid transitions, and its methods are called by Future::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:

#[derive(StateMachineFuture)]
enum MyStateMachine {
    // ...
}

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.

#[derive(StateMachineFuture)]
enum MyStateMachine {
    #[state_machine_future(start)]
    Start,

    // ...

    #[state_machine_future(ready)]
    Ready(MyItem),

    #[state_machine_future(error)]
    Error(MyError),
}

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.

#[derive(StateMachineFuture)]
enum MyStateMachine {
    #[state_machine_future(start, transitions(Intermediate))]
    Start,

    #[state_machine_future(transitions(Start, Ready))]
    Intermediate { x: usize, y: usize },

    #[state_machine_future(ready)]
    Ready(MyItem),

    #[state_machine_future(error)]
    Error(MyError),
}

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 Intermediate state variant's typestate is also named Intermediate. 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 enum for the possible states that can come after this state. This enum is named AfterX where X is the state's name. There is also a From<Y> implementation for each Y state that can be transitioned to after X. For example, the Intermediate state would get:
enum AfterIntermediate {
    Start(Start),
    Ready(Ready),
}

impl From<Start> for AfterIntermediate {
    // ...
}

impl From<Ready> for AfterIntermediate {
    // ...
}

Next, for the state machine as a whole, the custom derive generates:

  • A state machine Future type, which is essentially an enum of all the different typestates. This type is named BlahFuture where Blah is the name of the state machine description enum. In this example, where the state machine description is named MyStateMachine, the generated state machine future type would be named MyStateMachineFuture.

  • A polling trait, PollBordle where Bordle is this state machine description's name. For each non-final state TootWasabi, this trait has a method, poll_toot_wasabi, which is like Future::poll but specialized to the current state. Each method takes conditional ownership of its state (via RentToOwn) and returns a futures::Poll<AfterThisState, Error> where Error is 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 the MyStateMachine's polling trait, for example:

trait PollMyStateMachine {
    fn poll_start<'a>(
        start: &'a mut RentToOwn<'a, Start>,
    ) -> Poll<AfterStart, Error>;

    fn poll_intermediate<'a>(
        intermediate: &'a mut RentToOwn<'a, Intermediate>,
    ) -> Poll<AfterIntermediate, Error>;
}
  • An implementation of Future for that type. This implementation dispatches to the appropriate polling trait method depending on what state the future is in:

    • If the Future is in the Start state, then it uses <MyStateMachine as PollMyStateMachine>::poll_start.

    • If it is in the Intermediate state, then it uses <MyStateMachine as PollMyStateMachine>::poll_intermediate.

    • Etc...

  • A concrete start method for the description type (so MyStateMachine::start in this example) which constructs a new state machine Future type 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.

impl PollMyStateMachine for MyStateMachine {
    fn poll_start<'a>(
        start: &'a mut RentToOwn<'a, Start>
    ) -> Poll<AfterStart, MyError> {
        // Call `try_ready!(start.inner.poll())` with any inner futures here.
        //
        // If we're ready to transition states, then we should return
        // `Ok(Async::Ready(AfterStart))`. If we are not ready to transition
        // states, return `Ok(Async::NotReady)`. If we encounter an error,
        // return `Err(...)`.
    }

    fn poll_intermediate<'a>(
        intermediate: &'a mut RentToOwn<'a, Intermediate>
    ) -> Poll<AfterIntermediate, MyError> {
        // Same deal as above...
    }
}

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.

#[macro_use]
extern crate state_machine_future;
extern crate futures;

use futures::*;
use state_machine_future::*;

struct MyContext {

}

struct MyItem {

}

enum MyError {

}

#[derive(StateMachineFuture)]
#[state_machine_future(context = "MyContext")]
enum MyStateMachine {
    #[state_machine_future(start, transitions(Intermediate))]
    Start,

    #[state_machine_future(transitions(Start, Ready))]
    Intermediate { x: usize, y: usize },

    #[state_machine_future(ready)]
    Ready(MyItem),

    #[state_machine_future(error)]
    Error(MyError),
}

impl PollMyStateMachine for MyStateMachine {
    fn poll_start<'s, 'c>(
        start: &'s mut RentToOwn<'s, Start>,
        context: &'c mut RentToOwn<'c, MyContext>
    ) -> Poll<AfterStart, MyError> {

        // The `context` instance passed into `start` is available here.
        // It is a mutable reference, so are free to modify it.

        unimplemented!()
    }

    fn poll_intermediate<'s, 'c>(
        intermediate: &'s mut RentToOwn<'s, Intermediate>,
        context: &'c mut RentToOwn<'c, MyContext>
    ) -> Poll<AfterIntermediate, MyError> {

        // The `context` is available here as well.
        // It is the same instance. This means if `poll_start` modified it, those
        // changes will be visible to this method as well.

        unimplemented!()
    }
}

fn main() {
    let _ = MyStateMachine::start(MyContext { });
}

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. The one exception to this is when the state machine is in a ready or error state, where it will resolve normally when polled if the context has been taken.

That's it!

Example

Here is an example of a simple turn-based game played by two players over HTTP.

#[macro_use]
extern crate state_machine_future;

#[macro_use]
extern crate futures;

use futures::{Async, Future, Poll};
use state_machine_future::RentToOwn;

/// The result of a game.
pub struct GameResult {
    winner: Player,
    loser: Player,
}

/// Some kind of simple turn based game.
///
/// ```text
///              Invite
///                |
///                |
///                | accept invitation
///                |
///                |
///                V
///           WaitingForTurn --------+
///                |   ^             |
///                |   |             | receive turn
///                |   |             |
///                |   +-------------+
/// game concludes |
///                |
///                |
///                |
///                V
///            Finished
/// ```
#[derive(StateMachineFuture)]
enum Game {
    /// The game begins with an invitation to play from one player to another.
    ///
    /// Once the invited player accepts the invitation over HTTP, then we will
    /// switch states into playing the game, waiting to recieve each turn.
    #[state_machine_future(start, transitions(WaitingForTurn))]
    Invite {
        invitation: HttpInvitationFuture,
        from: Player,
        to: Player,
    },

    // We are waiting on a turn.
    //
    // Upon receiving it, if the game is now complete, then we go to the
    // `Finished` state. Otherwise, we give the other player a turn.
    #[state_machine_future(transitions(WaitingForTurn, Finished))]
    WaitingForTurn {
        turn: HttpTurnFuture,
        active: Player,
        idle: Player,
    },

    // The game is finished with a `GameResult`.
    //
    // The `GameResult` becomes the `Future::Item`.
    #[state_machine_future(ready)]
    Finished(GameResult),

    // Any state transition can implicitly go to this error state if we get an
    // `HttpError` while waiting on a turn or invitation acceptance.
    //
    // This `HttpError` is used as the `Future::Error`.
    #[state_machine_future(error)]
    Error(HttpError),
}

// Now, we implement the generated state transition polling trait for our state
// machine description type.

impl PollGame for Game {
    fn poll_invite<'a>(
        invite: &'a mut RentToOwn<'a, Invite>
    ) -> Poll<AfterInvite, HttpError> {
        // See if the invitation has been accepted. If not, this will early
        // return with `Ok(Async::NotReady)` or propagate any HTTP errors.
        try_ready!(invite.invitation.poll());

        // We're ready to transition into the `WaitingForTurn` state, so take
        // ownership of the `Invite` and then construct and return the new
        // state.
        let invite = invite.take();
        let waiting = WaitingForTurn {
            turn: invite.from.request_turn(),
            active: invite.from,
            idle: invite.to,
        };
        transition!(waiting)
    }

    fn poll_waiting_for_turn<'a>(
        waiting: &'a mut RentToOwn<'a, WaitingForTurn>
    ) -> Poll<AfterWaitingForTurn, HttpError> {
        // See if the next turn has arrived over HTTP. Again, this will early
        // return `Ok(Async::NotReady)` if the turn hasn't arrived yet, and
        // propagate any HTTP errors that we might encounter.
        let turn = try_ready!(waiting.turn.poll());

        // Ok, we have a new turn. Take ownership of the `WaitingForTurn` state,
        // process the turn and if the game is over, then transition to the
        // `Finished` state, otherwise swap which player we need a new turn from
        // and request the turn over HTTP.
        let waiting = waiting.take();
        if let Some(game_result) = process_turn(turn) {
            transition!(Finished(game_result))
        } else {
            let next_waiting = WaitingForTurn {
                turn: waiting.idle.request_turn(),
                active: waiting.idle,
                idle: waiting.active,
            };
            Ok(Async::Ready(next_waiting.into()))
        }
    }
}

// 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.
fn spawn_game(handle: TokioHandle) {
    let from = get_some_player();
    let to = get_another_player();
    let invitation = invite(&from, &to);
    let future = Game::start(invitation, from, to);
    handle.spawn(future)
}

Attributes

This is a list of all of the attributes used by state_machine_future:

  • #[derive(StateMachineFuture)]: Placed on an enum that describes a state machine.

  • #[state_machine_future(derive(Clone, Debug, ...))]: Placed on the enum that describes the state machine. This attribute describes which #[derive(...)]s to place on the generated Future type.

  • #[state_machine_future(start)]: Used on a variant of the state machine description enum. There must be exactly one variant with this attribute. This describes the initial starting state. The generated start method has a parameter for each field in this variant.

  • #[state_machine_future(ready)]: Used on a variant of the state machine description enum. There must be exactly one variant with this attribute. It must be a tuple-style variant with one field, for example Ready(MyItemType). The generated Future implementation uses the field's type as Future::Item.

  • #[state_machine_future(error)]: Used on a variant of the state machine description enum. There must be exactly one variant with this attribute. It must be a tuple-style variant with one field, for example Error(MyError). The generated Future implementation uses the field's type as Future::Error.

  • #[state_machine_future(transitions(OtherState, AnotherState, ...))]: Used on a variant of the state machine description enum. 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)] to stdout for 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.

More Repositories

1

dodrio

A fast, bump-allocated virtual DOM library for Rust and WebAssembly.
Rust
1,236
star
2

bumpalo

A fast bump allocation arena for Rust
Rust
1,018
star
3

wu.js

wu.js is a JavaScript library providing higher order functions for ES6 iterators.
JavaScript
864
star
4

generational-arena

A safe arena allocator that allows deletion without suffering from the ABA problem by using generational indices.
Rust
611
star
5

github-api

Javascript bindings for the Github API.
JavaScript
170
star
6

glob-to-regexp

Convert a glob to a regular expression
JavaScript
145
star
7

operational-transformation

JavaScript implementation of Operational Transformation
JavaScript
141
star
8

oxischeme

A Scheme implementation, in Rust.
Rust
128
star
9

id-arena

A simple, id-based arena
Rust
88
star
10

bacon-rajan-cc

Rust
86
star
11

mach

A rust interface to the Mach 3.0 kernel that underlies OSX.
Rust
80
star
12

bindgen-tutorial-bzip2-sys

A tutorial/example crate for generating C/C++ bindings on-the-fly with libbindgen
Rust
75
star
13

inlinable_string

An owned, grow-able UTF-8 string that stores small strings inline and avoids heap-allocation.
Rust
69
star
14

intrusive_splay_tree

An intrusive splay tree implementation that is no-std compatible and free from allocation and moves
Rust
65
star
15

synth-loop-free-prog

Synthesis of Loop-free Programs in Rust
Rust
62
star
16

fart

fitzgen's art
Rust
61
star
17

peepmatic

A DSL and compiler for generating peephole optimizers for Cranelift
Rust
59
star
18

scrapmetal

Scrap Your Rust Boilerplate
Rust
55
star
19

operational-transformation-example

Example app using Operational Transformation
JavaScript
51
star
20

wasm-smith

A WebAssembly test case generator
44
star
21

source-map-mappings

Parse the `mappings` field in source maps
Rust
39
star
22

chronos

Avoids JavaScript timer congestion
JavaScript
38
star
23

wasm-nm

List the symbols within a wasm file
Rust
38
star
24

associative-cache

A generic, fixed-size, associative cache
Rust
35
star
25

fast-bernoulli

Efficient sampling with uniform probability
Rust
32
star
26

zoolander

Pure Python DSL for CSS.
Python
28
star
27

django-wysiwyg-forms

WYSIWYG form editor/creator django app
JavaScript
25
star
28

winliner

The WebAssembly Indirect Call Inliner
Rust
25
star
29

minisynth-rs

Program synthesis is possible in Rust
Rust
24
star
30

derive_is_enum_variant

Automatically derives `is_dog` and `is_cat` methods for `enum Pet { Dog, Cat }`.
Rust
22
star
31

tempest

Tempest jQuery Templating Plugin
JavaScript
22
star
32

pfds-js

Purely Functional Data Structures in JS
JavaScript
22
star
33

histo

Histograms with a configurable number of buckets, and a terminal-friendly Display.
Rust
21
star
34

one-page-wasm

Rust
21
star
35

shuffling-allocator

Rust
20
star
36

rpn-js

A reverse polish notation --> JavaScript compiler demoing source maps
JavaScript
19
star
37

bugzilla-todos

Bugzilla todo list of reviews, flag requests, and bugs to fix
JavaScript
19
star
38

dwprod

Rust
16
star
39

tryparenscript.com

The code behind TryParenScript.com
JavaScript
16
star
40

is_executable

Is there an executable file at the given path?
Rust
15
star
41

rent_to_own

A wrapper type for optionally giving up ownership of the underlying value.
Rust
12
star
42

noodles

Asynchronous, non-blocking, continuation-passing-style versions of the common higher order functions in `Array.prototype`.
JavaScript
12
star
43

rust-conf-2019

Flatulence, Crystals, and Happy Little Accidents
JavaScript
11
star
44

life

John Conway's Game of Life (in Erlang)
Erlang
10
star
45

peeking_take_while

Rust
9
star
46

bufrng

An RNG that generates "random" numbers copied from a given buffer
Rust
9
star
47

wasm-summit-2021

Stuff related to my Wasm Summit 2021 talk
Rust
9
star
48

geotoy

Polygons in Contact + Glium
Rust
9
star
49

preduce

A parallel, language agnostic, automatic test case reducer
Rust
8
star
50

erl-ot

Simple example of Operational Transformation in Erlang
Erlang
8
star
51

longest-increasing-subsequence

A crate for finding a longest increasing subsequence of some input
Rust
8
star
52

cl-pattern-matching

Erlang/Haskell style pattern matching macros for Common Lisp.
Common Lisp
7
star
53

parenscript-narwhal

Integrates ParenScript with Narwhal and provides a ParenScript REPL.
JavaScript
7
star
54

reform

A Narwhal module for working with HTML forms, similar to Django's forms.
JavaScript
7
star
55

ada-scheme

Following along with Peter Michaux's Scheme from Scratch series (http://peter.michaux.ca/articles/scheme-from-scratch-introduction) in Ada
6
star
56

wasm-debugging-capabilities

Collecting requirements for WebAssembly debugging capabilities
HTML
6
star
57

couchdb-io

Io library for working with CouchDB (not yet implemented)
Io
5
star
58

cool-rs

Rust
5
star
59

souper-ir

A library for manipulating Souper IR
Rust
4
star
60

canvas-tool

experimental canvas tool; WORK IN PROGRESS
JavaScript
4
star
61

code-size-benchmarks

Rust and WebAssembly code size micro benchmarks
Rust
4
star
62

dodrio-todomvc-wasm-instantiation

Rust
4
star
63

clife

Conway's life, in Rust.
Rust
3
star
64

class_views

Class based views for django
Python
3
star
65

Blimp

A very simple django blogging app for my site. Blog + Simple = Blimp?
Python
2
star
66

mxr

Search the Mozilla Cross Reference from the command line
Python
2
star
67

tokio-timeit-middleware

Rust
2
star
68

tasks

A demo app I used for my presentation on server side JS.
JavaScript
2
star
69

kahn

A build tool for AMD-style Common JS projects
JavaScript
2
star
70

boffo

Boffo
Erlang
2
star
71

pineapple

Not quite sure yet
JavaScript
2
star
72

mach_o_sys

Bindings to the OSX mach-o system library
Rust
2
star
73

dateformat

A port of Steven Levithan's dateFormat to CommonJS.
JavaScript
2
star
74

mcmc-maze-solver

Rust
2
star
75

protolith

Just for exploratory fun, I decided to see what it takes to write a prototypical object system in Lisp.
Common Lisp
2
star
76

strace.js

Trace JS native calls
JavaScript
2
star
77

mini-meta-git

Very simple, lightweight git repository authentication and management (because gitosis just gets in my way).
2
star
78

tron

An online implementation of Tron, but you play with code instead of keys. Built with Wu.js, WebSockets, and Node.
JavaScript
2
star
79

aossoa

A crate providing a macro to abstract and automate SoA vs AoS
Rust
1
star
80

powereddit

Make reddit better.
JavaScript
1
star
81

servo-trace-dump

JS and CSS for Servo's HTML timelines
HTML
1
star
82

reharm

Prolog
1
star
83

pybeasttree

Library for parsing/consuming BEAST tree files
Python
1
star
84

django-query-analyzer

Analyzer of django query
Python
1
star
85

rust-words

Learning Rust. Read words from stdin and then print each unique word out in order and how many times it was found.
Rust
1
star
86

breakpoint-rs

Set breakpoints with the `breakpoint!()` macro.
Rust
1
star
87

ocean-noise

Makes soothing ocean noises.
JavaScript
1
star
88

bender

Create robust user interfaces with finite state machines.
JavaScript
1
star
89

hippocampus

A Lisp dialect targetting JavaScript
JavaScript
1
star
90

magritte

My just-for-fun Lisp in progress
Common Lisp
1
star
91

loader

A smart, parallel client-side Javascript loader written in ParenScript
Common Lisp
1
star
92

learn-your-scales

Flashcard-style webapp for learning musical scales!
JavaScript
1
star
93

music-thang

JavaScript
1
star
94

what-the-ffi-python-example

Rust
1
star
95

bang-bang-con-west-2020

Writing Programs! That Write Other Programs!!
HTML
1
star
96

pancakes

Still a WIP
Rust
1
star
97

reader-submit-to-hn

Greasemonkey script to add a link for submitting articles to Hacker News from Google Reader.
JavaScript
1
star