Skip to content
This repository has been archived by the owner on Apr 16, 2024. It is now read-only.

Non-deterministic FSM? #129

Closed
si14 opened this issue Apr 4, 2016 · 3 comments
Closed

Non-deterministic FSM? #129

si14 opened this issue Apr 4, 2016 · 3 comments
Labels

Comments

@si14
Copy link
Contributor

si14 commented Apr 4, 2016

Do you think it's reasonable or possible to allow non-deterministic FSMs in django-fsm? An example would be a following machine representing some charge with three states:

  • upcoming
  • successful
  • declined

Depending on an external factors (success of charge), "make_charge" signal/transition can lead either to "successful" or to "declined" state. However, this is true only assuming that "transition" is meant to be somewhat equivalent to a "signal" in a classic FSM parlance. If it's not the case, it's reasonable to emulate classic FSMs on top of django-fsm, but it will require quite a lot of boilerplate.

@kmmbvnr kmmbvnr added the feature label Apr 4, 2016
@kmmbvnr
Copy link
Collaborator

kmmbvnr commented Apr 4, 2016

The main question here, is how to express the non-determinism in a declarative way? It would be nice to keep ability to draw state diagrams.

I thought about something like this:

from django_fsm import RESULT_STATE

@transition(..., target=RESULT_STATE(STATUS.NEW_1, STATUS.NEW_2))
def doit(self):
    return STATUS.NEW_1

in case of RESULT_STATE as the target, the return value of a function should be one of the listed states.

The bad thing here, that the library capture a return value of the transition for own usage. But may be it's not a big issue for the end users.

@gergelypolonkai
Copy link

That’s exactly I’m looking for right now. My use case would be to get to different error states based on the exceptions thrown. I was thinking about using error=(((ExceptionClass1,ExceptionClass2,), 'state'), ((ExceptionClass3, ExceptionClass4), 'other-state')), although it looks strange now that I typed it :)

@si14
Copy link
Contributor Author

si14 commented May 11, 2016

After some thinking and code writing I'm no longer sure it's a good idea to put nondeterminism into @transition. If I understand the design correctly, @transition is supposed to be a pure model state -> model state function, therefore no model saving or side effects inside the transition (docs are a bit self-contradictory on this, btw). Given that large part of nondeterminism comes with side effects attached (e.g. successfulness of a charge), usage of a transition return value will encourage bad design.

Right now I'm solving the problem like this:

    @transition(field=payment_state,
                source=[PAYMENT_STATES.upcoming,
                        PAYMENT_STATES.delinquent],
                target=PAYMENT_STATES.charged)
    def _mark_as_charged(self):
        pass

    @transition(field=payment_state,
                source=[PAYMENT_STATES.upcoming,
                        PAYMENT_STATES.delinquent],
                target=PAYMENT_STATES.delinquent)
    def _mark_as_delinquent(self):
        pass

    def can_charge(self):
        return (fsm_can_proceed(self._mark_as_charged) and
                fsm_can_proceed(self._mark_as_delinquent))

    def charge(self):
        if not self.can_charge():
            raise TransitionNotAllowed(("can't charge when in state '{}'".
                                        format(self.payment_state)))

        charge_succeeded = StripeCharge.perform_charge(self)
        if charge_succeeded:
            self._mark_as_charged()
        else:
            self._mark_as_delinquent()
        self.save()

It's not pretty, but makes some sense IMO. However, I'm not sure it's possible to get away without first-class "signals" or something similar to what you did in Workflow.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants