Automat State Machines
the new kind of Automat
I’m a big fan of Automat for (explicit) state-machines in Python.
It can produce GraphViz -based diagrams directly from the code. In fact, mostly you can produce all the “scaffolding” of the machine alongside the diagram.
Then, when the diagram looks plausible, you “draw the rest of the owl” and write the method bodies.
There was however at least one problem: you couldn’t “re-enter” the machine. That is, you couldn’t produce an “input” to the machine from a state-transition.
Luckily, this bug is now fixed – alongside an extensive re-factor with a completely different API.
Glyph has a good example of why you might want to “re-enter” the state-machine in the documentation. I have also experienced this in some of the real state-machines I’ve written with Automat.
The New Way
Okay, great, so we love Automat and want to make state-machines.
In the old API you basically had a single class that “did” the state-machine – this included the inputs, outputs and state-transitions as well as any additional state you carried around.
The new API splits things up a little: the “inputs” are inside their own typing.Protocol class and the transitions are written as (decorated) functions. Outputs have been improved considerably and are now return values from transitions.
However, with all these bare functions lying around, it’s a little less obvious how to structure things.
Structural Proposal
I haven’t done much with the new API but here’s how I’ve been doing it so far.
There’s no real choice for the inputs: they go in a typing.Protocol-derived class. Since they’re “the public API” for your machine, this is just a normal class in your module.
Next, we have everything else.
There’s one catch: to have automat-visualize work properly, the “thing that builds the machine” has to be top-level visible. This thing is the output of calling TypeMachineBuilder.build().
I’ve been putting the “everything else” into its own method. This could – or could not – include the “state” class, as you see fit. So far, I’ve been leaving them in the top-level of the module too (but “private”).
Let’s look at the raw structure then:
import typing
import automat
import attrs
class ToggleButton(typing.Protocol):
def push_button(self) -> bool:
"""
push the button, returning whether it's now on or off
"""
@attrs.define
class _ToggleButtonState:
pass
def _create_toggle_machine():
builder = automat.TypeMachineBuilder(ToggleButton, _ToggleButtonState)
# define all our states; the first one is the initial state
on = builder.state("on")
# todo: state transition functions
return builder.build()
ToggleButtonMachine = _create_toggle_machine()
This gives an outline of where we’re going. Notice:
automat-visualizeworks, becauseToggleButtonMachineis at the top level. I’m a little conflicted about naming this like a class, but it acts as a constructor so … it’s a duck?- this machine doesn’t need state, but many do
- you could use
dataclassinstead of Attrs if you don’t like cool things - most of the “mess” is hidden in the
_create_toggle_machine()function, so more than one machine can live in a module
Now that you have some scaffolding, you can even run automat-visualize on the above. This is very boring, because without any state-transitions it doesn’t actually draw anything. However, if we bulk it out just a little bit it will (I’m showing just the new function here, not the whole thing):
def _create_toggle_machine():
builder = automat.TypeMachineBuilder(ToggleButton, _ToggleButtonState)
# define all our states; the first one is the initial state
on = builder.state("on")
off = builder.state("off")
# transitions
@on.upon(ToggleButton.push_button).to(off)
def turn_off(inputs: ToggleButton, core: _ToggleButtonState) -> bool:
pass
return builder.build()
The turn_off function is a one-liner to complete, but to emphasize the point here I’ve made its body pass – you can do this with all transition functions while you build up the diagram.
This is literally how I build state-machines in Automat (old or new API): make enough infrastructure to get automat-visualize working, and then add transitions and states until it looks plausible.
Then you can write some tests or “real” code that uses this, and do the inevitable iterating as things don’t work. I find it really valuable to be able to refer to a real (and nice looking) diagram that comes straight from the code. In other situations, I’d draw the state-diagram first “by hand” (i.e. with GraphViz directly) but then have to keep it in sync as code is written, with the inevitable consequences (it’s not always in sync!)
Conclusion?
I’m not sure if this is the final word here, but so far I like this structure.
Thoughts? Send them to this Mastodon thread please.
txtorcon
carml
cuv’ner