glyph / automat

Self-service finite-state machines for the programmer on the go.
MIT License
591 stars 65 forks source link

NoTransition error when going from state A to state A #123

Closed ghost closed 3 years ago

ghost commented 4 years ago

I am using an implementation in which a periodic interval is updating the state of my machine. The way it is implemented the state change is triggered no matter if the machine has that state already. i.e. if my machine is in state A and the periodic state update mechanism says it should be in state A it will trigger input for state A. I found it going against the pattern to write a transition case to go from state A into state A but this way NoTransition errors are being raised. Since I could not find much about it in the documentation I was wondering if there are best practices on approaching this kind of scenario with automat or if this should be implemented also to not raise an error in this case.

See attached some dummy code to shine some light on my implementation:

class LightSettings:
    _machine = MethodicalMachine()

    def __init__(self):
        self.update_state()

    # This is called periodically from outside
    def update_state(self):
        now = datetime.datetime.now()
        point_A = ...
        point_B = ...
        if now < point_A or now > point_B:
            self.became_B()
        elif point_A < now < point_B:
            self.became_A()

    @_machine.state(initial=True)
    def is_undetermined(self):
        pass

    @_machine.state()
    def is_A(self):
        pass

    @_machine.state()
    def is_B(self):
        pass

    @_machine.input()
    def became_A(self):

    @_machine.input()
    def became_B(self):

    @_machine.output()
    def _settings_B(self):
        print("Settings for B")

    @_machine.output()
    def _settings_A(self):
        print("Settings for A")

    is_undetermined.upon(
        became_A,
        enter=is_A,
        outputs=[_settings_A]
    )
    is_undetermined.upon(
        became_B,
        enter=is_B,
        outputs=[_settings_B]
    )
    is_A.upon(
        became_B,
        enter=is_B,
        outputs=[_settings_B]
    )
    is_B.upon(
        became_A,
        enter=is_A,
        outputs=[_settings_A]
    )
moshez commented 3 years ago

Hi @floanwelt

In general, loops are valid in some machines and invalid in others, so you do need to indicate which loops are valid. However, loops, and empty loops at that, are a common use case, and the current solution of

is_A.upon(became_A, enter=is_A, outputs=[])

can be made more ergonomic. This is what issue #17 covers. If both enter, and outputs were optional, this would be

is_A.upon(became_A)
is_B.upon(became_B)

which says that these self-loops are valid but should do nothing. I am closing this issue since explicit, but ergonomic, self-loops are covered by #17.