(Using names 'send' etc. from the article just for easier comparison.)
This is basically the same except not using co-routines, and I don't see what advantage using them brings?
Not using them has a clear advantage (IMO) that we don't have to initialise each state (`_create_q2()`) - which just seems strange to read, and a recipe for forgetting ('I just added a state for this, why are we still seeing this error...') since it essentially means a state is 'declared' in init, and defined in a method. But this is Python, and a compiler won't complain. Neither will your linter know^ that you should have called that method in `__init__`.
^(OK, you _might_ have a check that it's used anywhere ever, but there's a lot of good reasons for not having that - library code, abstractions, linter not clever enough to work out you have called things in a non-straightforward way, etc.)
An even better example is with the coroutine FSM by the same author here https://github.com/arpitbbhayani/fsm/blob/master/divisibilit... . It is cleaned up a lot by using functions instead of coroutines. Here each 'state' function takes a digit as input and returns the next state:
class Divisibility3FSM:
def __init__(self):
self.current_state = self.init
def send(self, digit):
self.current_state = self.current_state(digit)
def is_divisible(self):
return self.current_state == self.q0
def init(self, digit):
if digit in [0, 3, 6, 9]:
return self.q0
elif digit in [1, 4, 7]:
return self.q1
elif digit in [2, 5, 8]:
return self.q2
def q0(self, digit):
if digit in [0, 3, 6, 9]:
return self.q0
elif digit in [1, 4, 7]:
return self.q1
elif digit in [2, 5, 8]:
return self.q2
def q1(self, digit):
if digit in [0, 3, 6, 9]:
return self.q1
elif digit in [1, 4, 7]:
return self.q2
elif digit in [2, 5, 8]:
return self.q0
def q2(self, digit):
if digit in [0, 3, 6, 9]:
return self.q2
elif digit in [1, 4, 7]:
return self.q0
elif digit in [2, 5, 8]:
return self.q1
That makes more sense. And we could still break out states into functions without being quite so explicit as in the article (and without the __init__ problem I described):
def create_state_machine(self):
if yield == 'a':
yield from self.q2()
def q2(self):
while (char := yield) == 'b':
pass
if char == 'c':
self.match = True
Or something like that anyway, untested of course.
(NB that actually fixes a bug in yours in that it would continue processing 'b's after the 'c' - of course that's still a match for the regex, but the FSM should have stopped.)
Thanks for pointing that out, I think I fixed it. Not well tested ;)
I'm not sure if the way that you're using yield within the if and while statements works correctly. I like the idea of replacing state transitions with method calls but you'll run into recursion depth problems with a state machine that alternates between two states.
> I'm not sure if the way that you're using yield within the if and while statements works correctly.
You're right...
Perhaps I'm not so surprised that `if yield == val:` is a SyntaxError, but that `yield` can't be used as an rvalue to a walrus operator (that the `while` example essentially boils down to) surprises me:
>>> char = yield
File "<stdin>", line 1
SyntaxError: 'yield' outside function
>>>
>>> # i.e. OK, that's the error we want if it works
>>>
>>> got_b = (char := yield) == 'b'
File "<stdin>", line 1
got_b = (char := yield) == 'b'
^
SyntaxError: invalid syntax
You can use Brzozowski's derivatives of regular expressions[1] to lazily get a state machine from a RE. I made a simple FSM using a trampoline function[2] to imitate a kind of table-drive GOTO design.[3]
@arpitbbhayani - this is very interesting. you are one step away from building "reliable functions" - the basis of Azure Durable Functions and Uber Cadence.
Is it really more intuitive than something like:
(Using names 'send' etc. from the article just for easier comparison.)This is basically the same except not using co-routines, and I don't see what advantage using them brings?
Not using them has a clear advantage (IMO) that we don't have to initialise each state (`_create_q2()`) - which just seems strange to read, and a recipe for forgetting ('I just added a state for this, why are we still seeing this error...') since it essentially means a state is 'declared' in init, and defined in a method. But this is Python, and a compiler won't complain. Neither will your linter know^ that you should have called that method in `__init__`.
^(OK, you _might_ have a check that it's used anywhere ever, but there's a lot of good reasons for not having that - library code, abstractions, linter not clever enough to work out you have called things in a non-straightforward way, etc.)