about summary refs log tree commit diff
path: root/finito-core/src/lib.rs
blob: 0fdb2bd912b8ac1d3e8c465707e77bc0ed96ed45 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! # What & why?
//!
//! Most processes that occur in software applications can be modeled
//! as finite-state machines (FSMs), however the actual states, the
//! transitions between them and the model's interaction with the
//! external world is often implicit.
//!
//! Making the states of a process explicit using a simple language
//! that works for both software developers and other people who may
//! have opinions on processes makes it easier to synchronise thoughts,
//! extend software and keep a good level of control over what is going
//! on.
//!
//! This library aims to provide functionality for implementing
//! finite-state machines in a way that balances expressivity and
//! safety.
//!
//! Finito does not aim to prevent every possible incorrect
//! transition, but aims for somewhere "safe-enough" (please don't
//! lynch me) that is still easily understood.
//!
//! # Conceptual overview
//!
//! The core idea behind Finito can be expressed in a single line and
//! will potentially look familiar if you have used Erlang in a
//! previous life. The syntax used here is the type-signature notation
//! of Haskell.
//!
//! ```text
//! advance :: state -> event -> (state, [action])
//! ```
//!
//! In short, every FSM is made up of three distinct types:
//!
//!   * a state type representing all possible states of the machine
//!
//!   * an event type representing all possible events in the machine
//!
//!   * an action type representing a description of all possible
//!     side-effects of the machine
//!
//! Using the definition above we can now say that a transition in a
//! state-machine, involving these three types, takes an initial state
//! and an event to apply it to and returns a new state and a list of
//! actions to execute.
//!
//! With this definition most processes can already be modeled quite
//! well. Two additional functions are required to make it all work:
//!
//! ```text
//! -- | The ability to cause additional side-effects after entering
//! -- a new state.
//! > enter :: state -> [action]
//! ```
//!
//! as well as
//!
//! ```text
//! -- | An interpreter for side-effects
//! act :: action -> m [event]
//! ```
//!
//! **Note**: This library is based on an original Haskell library. In
//! Haskell, side-effects can be controlled via the type system which
//! is impossible in Rust.
//!
//! Some parts of Finito make assumptions about the programmer not
//! making certain kinds of mistakes, which are pointed out in the
//! documentation. Unfortunately those assumptions are not
//! automatically verifiable in Rust.
//!
//! ## Example
//!
//! Please consult `finito-door` for an example representing a simple,
//! lockable door as a finite-state machine. This gives an overview
//! over Finito's primary features.
//!
//! If you happen to be the kind of person who likes to learn about
//! libraries by reading code, you should familiarise yourself with the
//! door as it shows up as the example in other finito-related
//! libraries, too.
//!
//! # Persistence, side-effects and mud
//!
//! These three things are inescapable in the fateful realm of
//! computers, but Finito separates them out into separate libraries
//! that you can drag in as you need them.
//!
//! Currently, those libraries include:
//!
//!   * `finito`: Core components and classes of Finito
//!
//!   * `finito-in-mem`: In-memory implementation of state machines
//!     that do not need to live longer than an application using
//!     standard library concurrency primitives.
//!
//!   * `finito-postgres`: Postgres-backed, persistent implementation
//!     of state machines that, well, do need to live longer. Uses
//!     Postgres for concurrency synchronisation, so keep that in
//!     mind.
//!
//! Which should cover most use-cases. Okay, enough prose, lets dive
//! in.
//!
//! # Does Finito make you want to scream?
//!
//! Please reach out! I want to know why!

use std::mem;

/// Primary trait that needs to be implemented for every state type
/// representing the states of an FSM.
///
/// This trait is used to implement transition logic and to "tie the
/// room together", with the room being our triplet of types.
pub trait FSM where Self: Sized {
    /// A human-readable string uniquely describing what this FSM
    /// models. This is used in log messages, database tables and
    /// various other things throughout Finito.
    const FSM_NAME: &'static str;

    /// The associated event type of an FSM represents all possible
    /// events that can occur in the state-machine.
    type Event;

    /// The associated action type of an FSM represents all possible
    /// actions that can occur in the state-machine.
    type Action;

    /// `handle` deals with any incoming events to cause state
    /// transitions and emit actions. This function is the core logic
    /// of any state machine.
    ///
    /// Implementations of this function **must not** cause any
    /// side-effects to avoid breaking the guarantees of Finitos
    /// conceptual model.
    fn handle(self, event: Self::Event) -> (Self, Vec<Self::Action>);

    /// `enter` is called when a new state is entered, allowing a
    /// state to produce additional side-effects.
    ///
    /// This is useful for side-effects that event handlers do not
    /// need to know about and for resting assured that a certain
    /// action has been caused when a state is entered.
    ///
    /// FSM state types are expected to be enum (i.e. sum) types. A
    /// state is considered "new" and enter calls are run if is of a
    /// different enum variant.
    fn enter(&self) -> Vec<Self::Action>;

    /// `act` interprets and executes FSM actions. This is the only
    /// part of an FSM in which side-effects are allowed.
    fn act(Self::Action) -> Vec<Self::Event>;
}

/// This function is the primary function used to advance a state
/// machine. It takes care of both running the event handler as well
/// as possible state-enter calls and returning the result.
///
/// Users of Finito should basically always use this function when
/// advancing state-machines manually, and never call FSM-trait
/// methods directly.
pub fn advance<S: FSM>(state: S, event: S::Event) -> (S, Vec<S::Action>) {
    // Determine the enum variant of the initial state (used to
    // trigger enter calls).
    let old_discriminant = mem::discriminant(&state);

    let (new_state, mut actions) = state.handle(event);

    // Compare the enum variant of the resulting state to the old one
    // and run `enter` if they differ.
    let new_discriminant = mem::discriminant(&new_state);
    let mut enter_actions = if old_discriminant != new_discriminant {
        new_state.enter()
    } else {
        vec![]
    };

    actions.append(&mut enter_actions);

    (new_state, actions)
}