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.