A common use case for the application of state machines is to get rid of huge 'if-then-else'-constructs and process events 'context-sensitive', meaning that what happens when an event is received depends on the current state of the machine/model.
While this is probably not of interest for maria_hoffman any longer, google might lead someone here with the same intention:
Let's assume we want to build a simple bot that is capable of adding two numbers. We start with defining the necessary states.
states = ["INIT", "WAITING", "ADD_1", "ADD_2", "QUIT"]
We start from INIT
and have a WAITING
state where operation instruction are received. We could skip this one but our bot might be extended in the future to also support multiplication. In ADD_1
we expect the first number and in ADD_2
the second number for our sum. When in state QUIT
we want the system to shutdown.
Next, we need to define the actual transitions that should happen:
transitions = [
dict(trigger='next', source='WAITING', dest='ADD_1', conditions=lambda x: x == "add"),
dict(trigger='next', source='WAITING', dest='QUIT', conditions=lambda x: x == "quit"),
dict(trigger='next', source='WAITING', dest='WAITING', before="show_error"),
dict(trigger='next', source='ADD_1', dest='ADD_2', before="store_value"),
dict(trigger='next', source='ADD_2', dest='WAITING', before="get_result"),
dict(trigger='reset', source='*', dest='WAITING'),
]
First, we see that we have just two events: next
and reset
. What happens when next
is triggered, depends on the current state. In WAITING
we process three possibilities: First, when the parameter passed with event next
is equal to add
, we transition to ADD_1
and wait for the first number to proces. If the parameter is equal to quit
, we transition to QUIT
and shutdown the system. If both condition checks fail we will use the third transition which will exit and re-enter WAITING
and call a method called show_error
before doing so. When transitioning from ADD_1
to ADD_2
we call a function to store the passed value. We need to remember it for get_result
which is called when next
is received in state ADD_2
. Lastly, we have a reset event to roll back things if something did not work out.
Now we are almost done, we just need to define some prompts and the aforementioned methods show_error
, store_value
and get_result
. We create a simple model for this. The idea is to show prompts depending on the state that has been entered. on_enter_<state>
is the right tool for this job. We also intialize self.first
in __init__
as a field to store the value of the first number that is passed in ADD_1
:
class Model:
def __init__(self):
self.first = 0
def on_enter_WAITING(self, *args):
print("Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program:", end=' ')
def on_enter_ADD_1(self, *args):
print("Please enter the first value:", end=' ')
def on_enter_QUIT(self, *args):
print("Goodbye!")
def store_value(self, value):
self.first = int(value)
print("Please enter the second value:", end=' ')
def get_result(self, value):
val = int(value)
print(f"{self.first} + {val} = {self.first + val}")
def show_error(self, *args):
print("Sorry, I cannot do that.")
Note that when we want to pass arguments to callbacks, all callbacks need to be able to deal with it. The documentation of transitions
states:
There is one important limitation to this approach: every callback function triggered by the state transition must be able to handle all of the arguments. This may cause problems if the callbacks each expect somewhat different data.
So, when we don't need the actual input value, we just put *args
in the signature to communicate this.
That's it. Now we tie everything together and implement some rudimentary error checks and we are good to go. We create a model instance and pass it to the machine. When we receive input we pass it to the model via next
and let the model do the heavy lifting. While the model is not in state QUIT
we will wait for the next input:
model = Model()
machine = Machine(model, states=states, transitions=transitions, initial='INIT')
model.to_WAITING()
while not model.is_QUIT():
inp = input()
try:
model.next(inp)
except ValueError:
print("Oh no! Something went wrong. Let's try again!")
model.reset()
This could be a conversation with the bot:
Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program: add
Please enter the first value: 123
Please enter the second value: 4
123 + 4 = 127
Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program: call
Sorry, I cannot do that.
Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program: add
Please enter the first value: foo
Oh no! Something went wrong. Let's try again!
Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program: add
Please enter the first value: 123
Please enter the second value: baz
Oh no! Something went wrong. Let's try again!
Hello, if you want to add two numbers enter 'add'. Enter 'quit' to close the program: quit
Goodbye!