Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Transient states/transitions #43

Closed
davidkpiano opened this issue Jan 12, 2018 · 4 comments
Closed

[Feature] Transient states/transitions #43

davidkpiano opened this issue Jan 12, 2018 · 4 comments
Milestone

Comments

@davidkpiano
Copy link
Member

davidkpiano commented Jan 12, 2018

Best explained in Horrock's book:

Transient states are different from normal states because none of the transitions leaving such
a state is triggered by events. Instead, the transitions are simply conditions without an
associated event. Since there are no events associated with transitions leaving the state, on
entry to the state, the conditions on the event arrows are evaluated and the next state is
entered immediately: hence the transient nature of the state. In effect a transient state delays
the entry to the next state until the actions associated with an event have been executed.
Transient states are not part of the original statechart notation, but they are needed when
designing user interfaces that retrieve data from a database. They can also be used to
simplify designs.

Below is an example of a transient state, and how it can be useful:

Transient State Diagram

Proposed API:

Instead of explicitly defining a "transient state", we allow any state to implicitly become transient by introducing transient transitions, which are checked immediately upon entering the state. This has the same semantics of transient states as described in the book. The transient transition (or "immediate transition") will be based on an empty event string, since that best demonstrates that it comes from no event:

UPDATED: see #43 (comment)

// const updateMachine = Machine({
//   initial: 'G',
//   states: {
//     G: {
//       on: { UPDATE_BUTTON_CLICKED: 'E' },
//     },
//     E: {
//       on: {
//         // transient transition
//         '': {
//           D: { cond: ({ data }) => !data }, // no data returned
//           B: { cond: ({ status }) => status === 'Y' },
//           C: { cond: ({ status }) => status === 'X' }
//         }
//       }
//     },
//     D: {},
//     B: {},
//     C: {}
//   }
// });

Actions will be evaluated in this order (using the above example):

  1. onExit for G
  2. actions for G -> E (ON_BUTTON_CLICKED)
  3. onEntry for E
  4. actions for determined transient transition from E -> D (or B or C)
  5. onExit for E
  6. onEntry for determined next state (D or B or C)

And a transition will evaluate to the ultimately determined state:

const nextState = updateMachine.transition('G', 'UPDATE_BUTTON_CLICKED', {});
console.log(nextState.value);
// => 'D'

Thoughts? Feedback?

@mogsie
Copy link
Collaborator

mogsie commented Jan 13, 2018

Will it remain in the transient state if none of the guards match? So the state is not simply a transient state, but rather an "immediate exit".

I also have to point out that the order of evaluating guards is significant, so using an object might not be a good choice for this.

Would this solution also be used to e.g. deal with guards? The same problem exists there. When I write a state and want to handle an event with a few guards, then the various guards need to be evaluated in the right order in order for the behaviour to be correct.

Perhaps:

    E: {
      on: {
        // transient transition
        '': [
          { target: "D", cond: ({ data }) => !data }, // no data returned
          { target: "B", cond: ({ status }) => status === 'Y' },
          { target: "C", cond: ({ status }) => status === 'X' },
          "D"   // default
        ]
      }
    },

This could work for normal events too, right?

on: { FOO: [ { target: "foo", cond: ... } ] }

Only transition to foo if the contition holds, otherwise let the event be handled by parent states.

@davidkpiano
Copy link
Member Author

The reason I chose objects is because:

  • enforcement of constraint that transitions must be unique on the target state key
  • O(1) lookup vs O(n) lookup
  • guard cond functions should be pure
  • guard cond function order should not matter - if it does matter, that presumes that multiple conds can evaluate to true, which introduces nondeterminism into the statechart

The fourth point is the most important for me, and is the guiding principle for designing the API. We can't avoid non-determinism and uniqueness with arrays, but we can naturally with object/key pairs.

@mogsie
Copy link
Collaborator

mogsie commented Jan 13, 2018

enforcement of constraint that transitions must be unique on the target state key

This constraint causes the boolean logic strings / functions to become a lot more complicated than necessary. If I want to transition to X if foo, otherwise, transition to Y if bar, else transition to Z if not baz, else transition to X if somethingelse. That's four tests. I can write this using if tests:

if (foo) X
else if (bar) Y
else if (!baz) Z
else if (somethingelse) X

However, if I'm constrained to encode the first and last conditions in the same if-test I have to write each of them as mutually exclusive boolean expressions (so that at most one evaluates to true):

 X: { cond: ({ foo, bar, baz, somethingelse }) =>  (foo || (!foo && !bar && baz && somethingelse)) }

I don't see how that's any better; it's impossible for me to read this boolean logic and know what's happening; and it's impossible to maintain, I don't even know how to write the logic for Y and Z without using venn diagrams.

O(1) lookup vs O(n) lookup

The list of transitions that can be chosen from any given event in any particular state is in the order of magnitude 1 to 10, I think this qualifies as an premature optimization.

guard cond functions should be pure

I think in the arrays vs objects perspective, both have this aspect.

guard cond function order should not matter - if it does matter, that presumes that multiple conds can evaluate to true, which introduces nondeterminism into the statechart

It only introduces nondeterminism if you impose the constraint that any two states must only be joined by a single transition. Using an array leaves everything completely deterministic. The four if/else is completely deterministic, it's just a lot easier to write (and read).

@davidkpiano
Copy link
Member Author

Okay yeah, I can see how that can easily get complex @mogsie. So we introduce this syntax instead (taking the first example):

const updateMachine = Machine({
  initial: 'G',
  states: {
    G: {
      on: { UPDATE_BUTTON_CLICKED: 'E' },
    },
    E: {
      on: {
        // transient transition
        '': [
          { target: 'D', cond: ({ data }) => !data }, // no data returned
          { target: 'B', cond: ({ status }) => status === 'Y' },
          { target: 'C', cond: ({ status }) => status === 'X' },
          { target: 'F' } // default, or just the string 'F'
        ]
      }
    },
    D: {},
    B: {},
    C: {},
    F: {}
  }
});

I can see this working, and it does simplify the boolean logic. To @mogsie's point, it's easier to represent A, B, C, D than A, !A && B, !A && !B && C, !A && !B && !C && D.

This array-based syntax for determining the next transition should also be allowed for other actions, as well. It doesn't interfere with the object-based syntax (and actually, I can see it replacing object-based syntax in a future version).

Thanks for the insight, @mogsie.

@davidkpiano davidkpiano changed the title [feature] Transient states/transitions [Feature] Transient states/transitions Jan 23, 2018
@davidkpiano davidkpiano added this to the 4.1 milestone Jan 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants