pelican/
pelican.rs

1//&# PELICAN
2//&
3//&This example implements a PEdestrians-LIight-CONtrolled (pelican) crossing (almost) as suggested
4//&in [Introduction Hierarchical State Machines](https://barrgroup.com/embedded-systems/how-to/introduction-hierarchical-state-machines).
5//&There, they describe an implementation of a pelican crossing as hierarchical state machine in C using techniques from OOP.
6//&This example adapts these techniques specifically to rspl and Rust yielding a corresponding implementation in Rust.
7//&
8//&The intention of the example is to demonstrate rspl's applicability in event-driven programming (with hierarchical state machines, of course).
9//&
10//&Now that we have said what we are going to implement and why, let us explain our techniques before presenting the code, applying those techniques.
11//&To this end, we split the following discussion into three parts.
12//&First, we introduce the general design pattern of encoding state machines in rspl.
13//&After that, we address the problem of how to deal with the hierarchy aspect in Rust.
14//&Both is done without referring to the specific example of the pelican crossing.
15//&Then, third and last, before discussing the actual pelican code in place we briefly discuss the code's overall structure.\
16//&So, first, rspl's stream processors can encode finite state machines in a way reminiscent of the (type-)state pattern: every machine state is represented as a stream processor which behaves on the input as the transition function of the machine in that state.
17//&This works because rspl is somewhat CPS-ish in the sense that every stream processor has the one for further processing encoded in its arguments.\
18//&On top, stream processors have two properties making it possible to generalize the domain of the design pattern in a natural way.
19//&On the one hand, stream processors are able to output something and hence allow to even encode Mealy machines.
20//&On the other hand, stream processors can have side effects expanding the domain by effectful machines.\
21//&Now, while it is nice to be able to encode effectful (Mealy) machines instead of only ordinary finite state machines, having the effect implementations baked into the machine can be unfavorable for reasons of modularity and control.[^1]
22//&To mitigate those problems, a possible approach is to reflect all possible effects into the stream processor's output type.
23//&Then, the effects become a stream of capabilities the machine requires the operator to provide in order to make progress.
24//&This improves modularity since the machine logic is seperated from its side effects.
25//&Moreover, an operator decides how to operate the machine further when an effect occurs and thus makes control more explicit.
26//&Generally, the approach is in analogy to effect handlers.\
27//&After having introduced the design pattern for state machines let us address the hierarchy aspect next.
28//&First, note that state machines have a deficiency w.r.t. the DRY-principle: if there are several - let us say n - states which transition on a certain input to a certain state, then a naive implementation of the transition function repeats some code n times.
29//&For large state machines, as in event-driven programming, this can be a problem.
30//&The solution is to use hierarchical state machines which organize the states in a tree rather than a list.
31//&The n states from above would then have a common ancestor from which they can inherit the implementation of the shared behavior.\
32//&Now Rust's inheritance features are scarce and do not natively apply to the problem at discourse.
33//&But Rust's local functions, shadowing properties and macros can do the hierarchy-trick in rspl's stream processor approach to state machines.
34//&Namely, because function definitions can be arbitrarily nested, one can encode trees whose nodes hold a list of function definitions which are accessible from the node itself and its descendants.
35//&Furthermore, nodes can effectively redefine functions already defined in ancestors by shadowing.
36//&This manifests itself in the fact that a call to a function refers to the first implementation encountered when walking up the tree (which coincides with the lexically closest definition).
37//&On the whole, using local functions in such a way reflects OOP with the usual inheritance, though in a somewhat cumbersome manner.
38//&The point now is that the definition of states in our approach is via 'global elements' of stream processors, that is, via functions with no arguments which output a stream processor.
39//&So, it is clear how to implement a hierarchy of states.
40//&But it is also clear how to share transition behavior: just define a function carrying the implementation to be shared within the appropriate node.
41//&This is particularly useful if the actual transition is a pattern match on the input of the machine where the match arms call the lexically closest function with the name of the pattern.
42//&This idea of always capturing the right function for each case can then be abstracted away by a `case-capture` macro, finally completing the hack for hierarchy.\
43//&Last but not least, let us have a look at the structure of our pelican crossing implementation.
44//&Essentially, it consists of three parts: a module encapsulating the machine logic of the pelican crossing, a driver-function responsible for providing the pelican crossing's capabilities and a main-function simulating the input and setting up the driver for the pelican crossing.
45//&Here, the machine logic mainly determines when it needs which capability like resetting the actual lights or feeding back an event by processing the event stream.
46//&The driver then executes the actions like resetting the lights or feeding back an event by need.
47//&
48//&Let us now walk through the code together.
49
50//&```rust
51mod pelican_machine {
52    use rspl::combinators::map;
53    use rspl::StreamProcessor;
54
55    use std::fmt;
56
57    const LENGTH_VEHICLES_GREEN_MIN: u64 = 10000;
58    const LENGTH_VEHICLES_YELLOW: u64 = 1000;
59    const LENGTH_PEDESTRIANS_GREEN: u64 = 10000;
60    const LENGTH_BOTH_RED: u64 = 2000;
61
62    #[derive(Copy, Clone)]
63    pub enum Color {
64        Red,
65        Yellow,
66        Green,
67        Black,
68    }
69
70    // This type defines the input alphabet of the machine. The `Push`-event is intended to trigger
71    // a transition from a vehicles-green phase to a pedestrians-green phase, while a
72    // `Timeout`-event signals to the machine that it has been in a certain state long enough. The
73    // `Exit`-event tells the machine to shut down.
74    #[derive(Copy, Clone)]
75    pub enum Event {
76        Push,
77        Timeout,
78        Exit,
79    }
80
81    // This type defines the output alphabet of the machine. The latter's meaning is hopefully
82    // rather self-explanatory. Perhaps, just note that `EmitTimeoutAfter` is ought to be the
83    // capability of the machine to feed back a `Timeout`-event to its own input.
84    pub enum Capability {
85        SetVehicleLights(Color),
86        SetPedestrianLights(Color),
87        EmitTimeoutAfter(u64),
88        UnexpectedTimeout(&'static str),
89        CallForHelp,
90        Break,
91    }
92
93    type State<'a> = StreamProcessor<'a, Event, Capability>;
94
95    // This is the aformentioned macro crucial for behavioral inheritance.
96    macro_rules! case_capture_transition {
97        () => {
98            StreamProcessor::get(|event| match event {
99                Event::Push => push(),
100                Event::Timeout => timeout(),
101                Event::Exit => exit(),
102            })
103        };
104    }
105
106    // This macro is really just another name for the previous one to make a later hack easier to
107    // understand.
108    macro_rules! ignore {
109        () => {
110            case_capture_transition!()
111        };
112    }
113
114    // This macros sequences arbitrarily many put-stream processors. It is called `mealy` and not
115    // `puts` (which would be a better name in general) because its arguments are supposed to make
116    // up a list of capabilities ending with a transition.
117    macro_rules! mealy {
118        ($transition:expr) => {
119            $transition
120        };
121
122        ( $output:expr, $( $rest:expr ),+ ) => {
123            StreamProcessor::put($output, || mealy!($( $rest ),+))
124        };
125    }
126
127    // The rest of the module is essentially the definition of the states and transitions of the
128    // machine.
129
130    // This defines the initial (top-level) state.
131    pub fn on<'a>() -> State<'a> {
132        // This code means that when the capabilities have been handled from outside in that order,
133        // the next step is a transition to the operational state.
134        mealy!(
135            Capability::SetPedestrianLights(Color::Red),
136            Capability::SetVehicleLights(Color::Red),
137            operational()
138        )
139    }
140
141    fn operational<'a>() -> State<'a> {
142        // This defines the implementation for the `exit`-case shared by all substates of the
143        // operational state.
144        fn exit<'a>() -> State<'a> {
145            off()
146        }
147
148        fn vehicles<'a>() -> State<'a> {
149            fn vehicles_green_guard<'a>() -> State<'a> {
150                fn push<'a>() -> State<'a> {
151                    // This is the aformentioned hack to just ignore `push`-events in the
152                    // `vehicles_green_guard`-state.
153                    ignore!()
154                }
155                fn timeout<'a>() -> State<'a> {
156                    vehicles_green()
157                }
158
159                // The match in the following macro correctly 'captures' the local functions from
160                // the `vehicles_green_guard`-state and the `exit`-function defined in the
161                // `operational`-state because they are the lexically closest.
162                case_capture_transition!()
163            }
164
165            fn vehicles_green<'a>() -> State<'a> {
166                fn push<'a>() -> State<'a> {
167                    vehicles_green_pushed()
168                }
169                fn timeout<'a>() -> State<'a> {
170                    vehicles_green_timedout()
171                }
172
173                mealy!(
174                    Capability::SetVehicleLights(Color::Green),
175                    Capability::EmitTimeoutAfter(LENGTH_VEHICLES_GREEN_MIN),
176                    case_capture_transition!()
177                )
178            }
179
180            fn vehicles_green_pushed<'a>() -> State<'a> {
181                fn push<'a>() -> State<'a> {
182                    ignore!()
183                }
184                fn timeout<'a>() -> State<'a> {
185                    vehicles_yellow()
186                }
187
188                case_capture_transition!()
189            }
190
191            fn vehicles_green_timedout<'a>() -> State<'a> {
192                fn push<'a>() -> State<'a> {
193                    vehicles_yellow()
194                }
195                fn timeout<'a>() -> State<'a> {
196                    mealy!(
197                        Capability::UnexpectedTimeout("state: vehicles_green_timedout"),
198                        error()
199                    )
200                }
201
202                case_capture_transition!()
203            }
204
205            fn vehicles_yellow<'a>() -> State<'a> {
206                fn push<'a>() -> State<'a> {
207                    ignore!()
208                }
209                fn timeout<'a>() -> State<'a> {
210                    pedestrians()
211                }
212
213                mealy!(
214                    Capability::SetVehicleLights(Color::Yellow),
215                    Capability::EmitTimeoutAfter(LENGTH_VEHICLES_YELLOW),
216                    case_capture_transition!()
217                )
218            }
219
220            mealy!(
221                Capability::SetPedestrianLights(Color::Red),
222                Capability::EmitTimeoutAfter(LENGTH_BOTH_RED),
223                vehicles_green_guard()
224            )
225        }
226
227        fn pedestrians<'a>() -> State<'a> {
228            fn pedestrians_green_guard<'a>() -> State<'a> {
229                fn push<'a>() -> State<'a> {
230                    ignore!()
231                }
232                fn timeout<'a>() -> State<'a> {
233                    pedestrians_green()
234                }
235
236                case_capture_transition!()
237            }
238
239            fn pedestrians_green<'a>() -> State<'a> {
240                fn push<'a>() -> State<'a> {
241                    ignore!()
242                }
243                fn timeout<'a>() -> State<'a> {
244                    vehicles()
245                }
246
247                mealy!(
248                    Capability::SetPedestrianLights(Color::Green),
249                    Capability::EmitTimeoutAfter(LENGTH_PEDESTRIANS_GREEN),
250                    case_capture_transition!()
251                )
252            }
253
254            mealy!(
255                Capability::SetVehicleLights(Color::Red),
256                Capability::EmitTimeoutAfter(LENGTH_BOTH_RED),
257                pedestrians_green_guard()
258            )
259        }
260
261        vehicles()
262    }
263
264    fn error<'a>() -> State<'a> {
265        mealy!(
266            Capability::SetPedestrianLights(Color::Red),
267            Capability::SetVehicleLights(Color::Red),
268            Capability::CallForHelp,
269            map(|_| Capability::CallForHelp)
270        )
271    }
272
273    fn off<'a>() -> State<'a> {
274        mealy!(
275            Capability::SetPedestrianLights(Color::Black),
276            Capability::SetVehicleLights(Color::Black),
277            Capability::Break,
278            map(|_| Capability::Break)
279        )
280    }
281
282    // The cryptic strings in this implementation are just ANSI escape codes.
283    impl fmt::Display for Color {
284        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285            match *self {
286                Self::Red => write!(f, "\x1b[41m  \x1b[0m"),
287                Self::Yellow => write!(f, "\x1b[43m  \x1b[0m"),
288                Self::Green => write!(f, "\x1b[42m  \x1b[0m"),
289                Self::Black => write!(f, "[BLACK]"),
290            }
291        }
292    }
293}
294
295use pelican_machine::{Capability, Event};
296
297use rspl::streams::overeager_receivers::OvereagerReceiver;
298use rspl::streams::Stream;
299
300use std::thread;
301use std::time::Duration;
302
303use crossbeam::channel;
304use crossbeam::channel::Sender;
305
306enum Feedback {
307    TimeoutAfter(u64),
308}
309
310fn driver<S>(events: S, tfeedback: &Sender<Feedback>)
311where
312    S: Stream<Event>,
313{
314    // This code starts the machine.
315    let mut capabilities = pelican_machine::on().eval(events);
316
317    loop {
318        // The following match provides the machine with capabilities. Most notably, it tells the
319        // feedback loop to trigger a `Timeout`-event after some time.
320        match *capabilities.head() {
321            Capability::SetVehicleLights(color) => println!("Vehicles: {}", color),
322            Capability::SetPedestrianLights(color) => println!("Pedestrians: {}", color),
323            Capability::EmitTimeoutAfter(length) => {
324                tfeedback.send(Feedback::TimeoutAfter(length)).unwrap();
325            }
326            Capability::UnexpectedTimeout(message) => {
327                eprintln!("log: unexpected timeout event ({})", message);
328            }
329            Capability::CallForHelp => {
330                println!("Call for help!");
331                break;
332            }
333            Capability::Break => break,
334        };
335        capabilities = capabilities.tail();
336    }
337}
338
339fn main() {
340    fn event_emitter(length: u64, tevents: &Sender<Event>, event: Event) {
341        thread::sleep(Duration::from_millis(length));
342        tevents.send(event).unwrap();
343    }
344
345    // The input/event stream is encoded as `OvereagerReceiver` which allows to easily implement
346    // feeding back by essentially connecting `rfeedback` with `tevents`.
347    let (tevents, events) = OvereagerReceiver::channel(0, Event::Push);
348    let (tfeedback, rfeedback) = channel::unbounded();
349
350    let _input_simulator = thread::spawn(move || {
351        let tevents_feedback = tevents.clone();
352
353        let _feedback = thread::spawn(move || loop {
354            match rfeedback.recv().unwrap() {
355                Feedback::TimeoutAfter(length) => {
356                    event_emitter(length, &tevents_feedback, Event::Timeout);
357                }
358            }
359        });
360
361        for _ in 0..10 {
362            event_emitter(5000, &tevents, Event::Push);
363            event_emitter(500, &tevents, Event::Push);
364        }
365
366        event_emitter(0, &tevents, Event::Exit);
367    });
368
369    driver(events, &tfeedback);
370}
371//&```
372
373//&The closing remarks shall just outline possible future work on event-driven programming with rspl: one thing which is conceivable is to develop some sort of domain-specific language helping with implementing arbitrary hierarchical state machines.
374//&It could consist of only some clever macros, but it could also be something more sophisticated like an UML-like language which is compiled to Rust with rspl.
375//&Another - admittedly somewhat more fantastical - possibility is a library of generic rspl-encoded machines which can be specialized by client code according to their needs by providing capabilities.
376
377//&[^1]: See monads and effect handlers in that regard.