Help with coroutine-based state machines?

Steven Taschuk staschuk at telusplanet.net
Thu May 29 17:40:08 EDT 2003


Quoth Alan Kennedy:
  [...]
> def __init__(self):
> 	statenames = ['state1', 'state2', 'state3', 'state4']
> 	for name in statenames:
> 		# Turn each generator-function into an instantiated
>                 # generator-iterator
> 		self.__dict__[name] = eval ('self.%s()' % name)
  [...]
> I'm not happy with the way that I have to explicitly list the state
> names. I tried to find a way to differentiate between functions and
> generator-functions while reflecting on the instance' dictionary.

I'd just use a naming convention.

    def __init__(self):
        for name in self.__dict__:
            if name.startswith('state_'):
                self.__dict__[name[6:]] = self.__dict__[name]()

Then

    def state_foo(self):
        ...

causes an iterator to be added as self.foo.

  [...]
> class FSM:
  [...]
>     def idle(self):
>         while 1:
>             #    We could wait for some condition here.
>             print "Idle state: %d" % self.counter
>             yield self.start
> 
>     def start(self):
>         while 1:
>             self.counter = self.startnum
>             print "Start state: %d" % self.counter
>             yield self.increment
  [...]

All your state function are of this form, I note:

    while 1:
        # do some calculation, determining next_state
        yield next_state

There's not much point to using generators in such a case, imho;
just use normal functions and return the next state:

    class Squares(object):
        def __init__(self, start, end):
            self.startnum = start
            self.endnum = end
        def start(self):
            self.counter = self.startnum
            return self.increment
        def increment(self):
            print self.counter**2
            self.counter += 1
            if self.counter >= self.endnum:
                return None
            else:
                return self.increment
        def run(self):
            for x in self.iterrun():
                pass
        def iterrun(self):
            # *This* is where a generator is handy.
            self._state = self.start
            while state is not None:
                self._state = self._state()
                yield None # or something more useful

(This gives suspend/resume, btw; for example

    machine = Squares(3, 100).iterrun()
    while 1:
        for i in range(5):
            machine.next()
        # do other stuff, possibly halt, etc.

pauses the machine every five state changes to do something else.)

Moreover, when the state functions are generators, you can write
"states" such as

    def foo(self):
        for nextstate in [self.state0, self.state1, self.state2]:
            yield nextstate

But this is not a state in the normal sense of "state machine";
each time you reach this "state", the machine's behaviour is
different, due solely to differences in the history of execution
(instead of due to, e.g., differences in input, which would be
legitimate).  The real state of the machine is the combination of
the states of all of the iterators.

Besides, one of the joys of generators is that they let you *get
rid of* state machines.  The whole machine above is better as

    def squares(start, end):
        for i in range(start, end):
            yield i**2

    for val in squares(3, 6):
        print val

(Note that you get suspend/resume for free here too.)  The states
are now provided by the normal control flow structures of the
language, which is much much easier to read.

  [...]
> Which is why python generators are only "semi"-coroutines, as
> opposed to full coroutines. Because, without a dispatcher function,
> you can only yield control to the calling function, rather than any
> resumable state that you have a reference to. [...]

In part, yes.  But even with the dispatcher, you can't call
another function and have *it* transfer control to another
coroutine, suspending the whole call stack.  (Not transparently,
anyway; it could be done by replacing all calls to functions with
yields to a dispatcher.)

  [...]
-- 
Steven Taschuk                               staschuk at telusplanet.net
"What I find most baffling about that song is that it was not a hit."
                                          -- Tony Dylan Davis (CKUA)





More information about the Python-list mailing list