(a.k.a concurrent states or orthogonal regions)
Usually within a statecharts, all states are tightly related and mutual
exclusive to each other (e.g. uploading
and uploaded
in a file
application.).
However, in lots of real world complex applications, there could be multiple "regions" of the application that are loosely related, or not related at all, but is always used/reasoned together in a module/screen/subsystem.
For instance, in a files management application, there could be
The files list could have the following states:
list-loading
: requesting the list of files from the serverlist-loaded
: successfully get the list of fileslist-load-failed
: error when requesting the list of files, e.g. ajax request
fails because of network errorThe files property has the following states:
props-idle
: no file is selected, so nothing to showprops-loading
: file is selected, and requesting the details of the file from
the serverprops-loaded
props-load-failed
Most people would feel comfortable to model this screen with two statecharts. But as more features are added, we'll need to create and maintain and reason about more and more disparate statecharts. For instance, each file could have a list of comments, which would pop out when a user clicks a "show comments" button. Of course we can just add one more new statecharts, but it's obvious to conclude that this doesn't look that appealing, because:
id
, a call to
fsm/machine
and fsm.rf/integrate
(see [re-frame
integration]({{< relref "docs/integration/re-frame.md" >}}) for details.)Parallel states (a.k.a concurrent states) is a mechanism in statecharts that could be used to use a single statecharts to model different parts of an application that doesn't depend on each other. Conceptually:
;!zprint {:format :on :map {:justify? true} :pair {:justify? true}}
{:id :file-app
:type :parallel ;; (1)
:context {:selected-file-name nil}
:regions ;; (2)
{:main {:initial :loading ;; (3)
:states {:loading {:on {:success-load-files :loaded
:fail-load-files :load-failed}}
:loaded {}
:load-failed {}}}
:props {:initial :idle
:states {:idle {:on {:file-selected :loading}}
:loading {:on {:success-load-props :loaded
:fail-load-props :load-failed}}
:loaded {}
:load-failed {}}}}
:comments {:initial :idle
:states {:idle {:on {:show-comments :loading}}
:loading {:on {:success-load-comments :loaded
:fail-load-comments :load-failed}}
:loaded {}
:load-failed {}}}}
(1) Use {:type :parallel}
to define a parallel state node
(2) Define the child regions in the :regions
key.
(3) Each child node of a parallel node must be a hierarchical state node.
In the example above, the root node of the statecharts is a parallel node. However you can put a parallel node anywhere in the state chart, e.g.:
{:id :hierarchical-parallel-demo
:initial :p2
:states ;; (1)
{:p1 ;; (2)
{:initial :p11
:states {:p11 {:on {:e12 :p12}}
:p12 {}}}
;; p2 is a hierarchical parallel node
:p2 ;; (3)
{:type :parallel
:regions
{:p2.a ;; (4)
{:initial :p2.a1
:states {:p2.a1 {:on {:e12 :p2.a2}}
:p2.a2 {}}}
:p2.b ;; (5)
{:initial :p2b2
:states
{:p2b1 {:on {:e231 :p2b2}}
;; parallel nest level depth +1
:p2b2 {:type :parallel ;; (6)
:regions {:p2b2.a {:initial :p2b2.a1
:states {:p2b2.a1 {}}}
:p2b2.b {:initial :p2b2.b1
:states {:p2b2.b1
{}}}}}}}}}}}
(1) The root node is a hierarchical node, with two regions :p1
and :p2
(2) :p1
is a hierarchical node
(3) :p2
is a parallel node with two regions :p2.a
and :p2.b
(4) :p2.a
is a hierarchical node
(5)(6) :p2.b
is a hierarchical node, but one of its children :p2b2
is a
parallel node.
We can build arbitrary complex statecharts this way, but it's highly discouraged because it makes the statecharts harder and harder to reason about.
{:r1 :r1-state :r2 :r2-state}
[:s1 :s1.1]
. However, if :s1.1
is a parallel node and has two regions
:r1
and :r2
, then it would be represented as
[:s1 {:s1.1 {:r1 :r1-state :r2 :r2-state}}]
.Nothing.
Some may say "it's more complex". But the complexity is a result of the inheritent complexity of the application itself, not introduced by using parallel nodes in the statecharts. The alternative is to use multiple smaller statecharts. However to keep track and reason about all of these smaller statecharts introduces extra cost both in your code and in your mind.
Some may worry about "there would be a performance impact", since for a parallel state, each event is dispatched to all its child states and in lots of cases some events is only handled by one child state. However, IMO this is hardly a problem given today's hardware technology, unless you're building some nano-second HFT system. For most applications, clj-statecharts takes less than 1ms to process an event.
Note that in xstate the regions are still represented in the states
key, which
I think is not a good choice since states
is also used to represent the
children of hierarchical states. So in clj-statecharts we use the :regions
key.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close