3 ms·
I have second thoughts on the author's assertion that "When it’s possible to store state in code instead, that often leads to a clearer program." I have found
by kccqzy 3y ago
I have second thoughts on the author's assertion that "When it’s possible to store state in code instead, that often leads to a clearer program."
I have found that an explicit state machine can be more readable when there are many states. When there are too many states, a straightforward transformation to control flow often cannot eliminate all the "goto"s like the author successfully did. Even if they were eliminated, there are usually too many nested "if" conditions and the like to be easily readable. That's the kind of spaghetti code that first inspired a ban on goto. (And that's not even including cases when you are writing code to a spec that's a state machine, like the TCP state machine.)
What I personally like, sometimes, is to make the state variable an enum and give names to be states. State 0 becomes kBeforeStart, state 1 becomes kInsideQuote, state 2 becomes kAfterBackslash. It suddenly now becomes intuitive. If you've written any kind of emulator or interpreter it's even more intuitive, because the state machine is just the DFA expanded from the regular expression. If I'm using a language with guaranteed tail calls, I can also replace each case in that switch by a separate named function.
It's possible to write clear and readable code in either style. Doing so requires skill and expertise, of which I don't think the author lacks.
- gpderetta 3y agoCompletely agree. When control flow gets messy, materializing it in explicit state variables is in my experience a significant improvement.
- ww520 3y agoYes. The second version is difficult to understand when there's a need to add new states or modify the states. The first version has a straightforward structure and shape, making it easier to understand and update the state machine.
- barrkel 3y agoI don't agree. The second version is the typical way hand-written compiler lexers are written, and they're straightforward to add new "states" to, or to modify the states. The program counter is where you're "at", and the state space is normally straightforward. In fact it only gets complicated when additional non-control-flow state has been added. A typical hand-written lexer looks a bit like this (EOF testing omitted for clarity): // skip whitespace while isWhiteSpace(peekChar()) nextChar() // determine token type switch (peekChar()) case isAlpha start := currentPosition() nextChar() while isAlphaNumeric(peekChar()) nextChar() tokenType = IDENT tokenValue = substring(start, currentPosition()) case isNumeric start := currentPosition() nextChar() while isNumeric(peekChar()) nextChar() tokenType = INT tokenValue = substring(start, currentPosition()) tokenAsInt = int(tokenValue) case '+' nextChar() tokenType = PLUS // and so on Adding new cases to this is trivial. Nested context-specific lexical details (like escape codes in strings) aren't merged with a global state machine, and can be handed off to subroutines, enabling a modularity which is less confusing than having multiple state machines.
- ww520 3y agoIsn’t what you wrote the first version?