I'm working on some embedded code using C. Various pieces of functionality need non-blocking stateful functions, which are mostly implemented using a switch on various states. For example, a modem connection manager (pseudo-code):
void manage_connection(void) {
static states state = IDLE;
switch (state) {
case IDLE:
if (connection_requested()) state = BACKOFF;
break;
case BACKOFF:
if (random_delay_timeout()) {
do_connect();
state = CONNECTED;
}
break;
case CONNECTED:
result = send_data();
if (result == DATA_SENT) {
state = IDLE;
} else if (result == ERROR) {
// Go straight to backoff state and connect ASAP
state = BACKOFF;
}
break;
}
}
State machines are nasty at the best of times, so I've started to move to a different paradigm using coroutine-style functions, using a "yield" statement. Of course, C doesn't provide a "yield" statement, but I'm using protothreads to achieve this. Protothreads is great, and gives me exactly what I want to implement the above non-blocking state machine in a structured-programming style. The above function roughly translates to (pseudo-code):
void manage_connection(void) {
WAIT_UNTIL(connection_requested()); // or, while(!condition) yield;
WAIT_UNTIL(random_delay_timeout());
do_connect();
WAIT_UNTIL((result = send_data()));
if (result == DATA_SENT) {
// RESTART starts from top. It is part of the protothreads API.
RESTART();
} else if (result == ERROR) {
// This is the part which doesn't translate so well.
// Could use an ugly goto:
goto backoff;
// Or similar alternatives, none of which scale well:
// - Set a flag to skip the states I don't want, and RESTART();
// - Use while loops or other control logic to branch appropriately.
}
}
As you can see, the protothreads version is, on the whole, much clearer & simpler in terms of the flow of logic (once you learn how protothreads work, and look past some very minor boilerplate which I haven't included in the example).
But jumping to a particular state in a state machine is a common pattern that I'm come across, which the state machine version handles elegantly, but the procedural version does not. Of course, that's why the procedural version is less bug-prone and clearer -- because you can't jump all over the place on a whim (except for goto, which I'd like to avoid for the usual reasons).
However, when you have a good reason to want to jump, it adds significant bug-prone lack of clarity to what is otherwise a very straightforward function.
Obviously there are plenty ways to achieve the necessary functionality, but none of them scale well to more than a few states. Bear in mind this is the simplest possible example -- my real-world functions fill half a screen.
What are some ways to solve this problem in a way that doesn't muddy the otherwise-lucid flow of logic?
result = STATE_MACHINE[state]();
. That's it. Works just fine. – user29079 May 8 '12 at 14:01