Xstate: Support activities natively

Created on 21 Jan 2018  Â·  9Comments  Â·  Source: davidkpiano/xstate

The original Statecharts paper has an entire chapter devoted to actions and activities. An excerpt:

An activity always takes a nonzero amount of time, like beeping, displaying, or executing lengthy computations. Thus, activities are durable-they take some time-whereas actions are instantaneous. In order to enable statecharts to control activities too, we need two special kinds of actions to start and stop activities. Accordingly, with each activity X we associate two special new actions, start(X) and stop(X), and a new condition active(X), all with the obvious meanings.

This hasn't carried forward in SCXML, SCION or xstate; there are hooks to start and stop things, but the notion of activities has been lost, at least it's not modeled anywhere.

The paper describes an activity as, e.g. "beeping" when in the "beeping" state, so the common solution is simply to use the name of the active state(s) to determine which UI to show. This (IMHO) leads to brittleness and the inability to do any sort of refactoring of the statechart (e.g. introduce substate) without considering how that new state might affect the UI.

Example state definition

loading: {
  on: { results: "show" },
  active: [ "http_request", "loading_ui" ],
}
  • I'm not sure about bikeshedding activities or activity or active. I prefer "active" since it's shortest, and more declarative-feeling.
  • A string can be passed instead of an array, when there's only one activity.

The _user_ of the statechart would use http_request to start the activity of making a HTTP request, while the loading_ui would cause the UI to display some "loading" state.

This could then easily be refactored to e.g. delay the "loading" _view_ by half a second.

loading: { on: { results: next },
  active: "http_request",
  states: {
    loading_1: {
      after: {
        0.5: loading_2
      } 
    },
    loading_2: {
      active: "loading"
    }
  }
}

You could then also easily delay the _exit_ of the loading state by half a second, so that the loading state doesn't flash by specializing the loading_2 state.

Usage:

State gets a new property activities with three properties:

  • start — an array of strings indicating which activities that need to start
  • stop — an array of strings indicating which activities that need to start
  • active — an array of strings indicating which activities that are currently active
newstate = transition(state, event, extradata);
// in the first example
console.log(newstate.activities.start); // [ "http_request", "loading" ]
// in the second example (after refactoring
console.log(newstate.activities.start); // [ "http_request" ]
// "after 0.5 seconds"
console.log(newstate.activities.start); // [ "loading" ]
console.log(newstate.activities.active); // [ "http_request", "loading" ]
// when results arrive, both activites are stopped.
console.log(newstate.activities.start); // [ ]
console.log(newstate.activities.stop); // [ "loading", "http_request" ]

Like entry/exit handlers, the activities started and stopped should be listed in the same order: When entering states, activites are started from the outermost states inward, and conversely when exiting states, activites are stopped from the innermost going outwards.

It paves the way for guard conditions that check if some activity is ongoing, preferably without needing to use a function declaration, but that could be food for a separate issue. I'm thinking something along the lines of is_active(loading)

This is not be a breaking change

Tjhis is not part of the SCXML specification, activites aren't mentioned in the spec. It is probably possible to convert activities as described here into pure entry/exit actions that trigger start/stopping of activities, but this becomes very verbose, and therefore probably not used so much.

enhancement

Most helpful comment

Okay, semifinal syntax - given a statechart like this:

const lightMachine = Machine({
  initial: 'green',
  states: {
    // ... green, yellow
    red: {
      initial: 'walk',
      activities: ['activateCrosswalkLight'],
      states: {
        walk: { on: { PED_WAIT: 'wait' } },
        wait: {
          activities: ['blinkCrosswalkLight'],
          on: { PED_STOP: 'stop' }
        },
        stop: {}
      }
    }
  }
}

The following would be returned in the State instance regarding activities:

  • nextState = transition('yellow', 'TIMER')

    • state: 'red.walk'

    • start: ['activateCrosswalkLight']

    • active: []

    • stop: []

  • nextState = transition(nextState, 'PED_WAIT')

    • state: 'red.wait'

    • start: ['blinkCrosswalkLight']

    • active: ['activateCrosswalkLight']

    • stop: []

  • nextState = transition(nextState, 'PED_STOP')

    • state: 'red.stop'

    • start: []

    • active: ['activateCrosswalkLight']

    • stop: ['blinkCrosswalkLight']

  • nextState = transition(nextState, 'TIMER')

    • state: 'green'

    • start: []

    • active: []

    • stop: ['activateCrosswalkLight']

Does this make sense @mogsie @lmatteis ?

All 9 comments

What are the semantics of activities? They are not clear in Harel's paper and couldn't find much info online about them.

From the statechart's perspective (using your first example), I send the statechart an event that transitions into the "loading" state. As an output I get back a "start 'http_request' and start 'loading'" -- the outside world then interprets that however it wants, perhaps by actually making an http request and perhaps by setting loading to true.

So the semantics are that: whenever entering a state, its activities are started, and whenever exiting a state, its activities are stopped. If that's the case, indeed, they can make onEntry/onExit less verbose.

Now it's unclear how your API is going to work since xstate is stateless. Regarding the timeouts: who's actually going to trigger the transition after 0.5 seconds?

I believe the extensions I am proposing won't remove the "statelessness" and "pure function" that is xstate. xstate won't actually _start_ or _stop_ any activities, simply tell you the user that "now it's time to stop this activity" and "now it's time to start that activity"

The timeout syntax is proposed in a different issue (#36), and that's also a stateless proposal, I believe.

Sounds good! I'm more in favor of using the word "throughout" rather than "active" for the statechart definition. Then for the output of the statechart, having a new activities property along with the start/stop/active properties looks good. In Harel's words:

We shall also allow the association of actions with the exit from a state, and will also allow to specify that an activity will be carried out continuously throughout the system’s being in the state. In other words, saying that the activity X is carried out throughout state A is just like saying that the action start(X) is carried out upon entering A, and stop(X) upon leaving A.

screen shot 2018-01-21 at 23 31 41

Wondering still how the statechart will let you know about a new action/activity after 0.5 seconds. But I guess I can ask that in the other issue.

More literature (especially in other implementations or research papers) would be greatly appreciated for this. If an activity can be equivalently represented as a series of actions (and possibly states), I'd prefer some sort of syntactic sugar to represent them. Otherwise, if an activity is a completely new primitive, let's discuss that.

@davidkpiano I guess the main point of activities is for things that not only need to start (such as actions), but also need to be stopped. Say for instance a "loading" indicator. You want to start the loading, but also stop it. Sure you can have your own syntactic sugar, as you say, and have appropriate startLoading and stopLoading actions, but having specific operators is less verbose -- I think that's the point @mogsie was trying to make.

Also without start/stop/active you end up using actions with all kinds of name that essentially do the same stuff: showLoading, makeHttpRequest, cancelHttpRequest, visualizeBanner. All these can be converted into: start(loading), start(httpRequest), stop(httpRequest), start(banner).

With throughout (or active as @mogsie was saying) this can go even further and instead of having onEntry: start(loading), onExit: stop(loading), you'd simply have { throughout: loading }.

Thanks for the clarification. 🚲 shedding here:

loading: {
  on: { results: "show" },
  activities: [ "http_request", "loading_ui" ],
}

I like "activities" because 1) it's more declarative, 2) less likely to get confused with active traditionally being a boolean property (e.g., active: true), and 3) provides a hint to the developer that it can take multiple activities.

I like the addition of activities: { start, stop, active }, we'll keep that syntax proposal as-is. Also, the activities property on the returned State object is related to the activities prop on the state configurations, so there's less to memorize (and it's easier to make the mental connection).

I also started with the word activities in my first draft, in both the statechart definition, and the properties on State, so I guess I concur with the color of the bikeshed 🎨

Okay, semifinal syntax - given a statechart like this:

const lightMachine = Machine({
  initial: 'green',
  states: {
    // ... green, yellow
    red: {
      initial: 'walk',
      activities: ['activateCrosswalkLight'],
      states: {
        walk: { on: { PED_WAIT: 'wait' } },
        wait: {
          activities: ['blinkCrosswalkLight'],
          on: { PED_STOP: 'stop' }
        },
        stop: {}
      }
    }
  }
}

The following would be returned in the State instance regarding activities:

  • nextState = transition('yellow', 'TIMER')

    • state: 'red.walk'

    • start: ['activateCrosswalkLight']

    • active: []

    • stop: []

  • nextState = transition(nextState, 'PED_WAIT')

    • state: 'red.wait'

    • start: ['blinkCrosswalkLight']

    • active: ['activateCrosswalkLight']

    • stop: []

  • nextState = transition(nextState, 'PED_STOP')

    • state: 'red.stop'

    • start: []

    • active: ['activateCrosswalkLight']

    • stop: ['blinkCrosswalkLight']

  • nextState = transition(nextState, 'TIMER')

    • state: 'green'

    • start: []

    • active: []

    • stop: ['activateCrosswalkLight']

Does this make sense @mogsie @lmatteis ?

Done in #56

Was this page helpful?
0 / 5 - 0 ratings

Related issues

3plusalpha picture 3plusalpha  Â·  3Comments

hnordt picture hnordt  Â·  3Comments

laurentpierson picture laurentpierson  Â·  3Comments

dakom picture dakom  Â·  3Comments

bradwoods picture bradwoods  Â·  3Comments