Skip to content

Latest commit

 

History

History
241 lines (190 loc) · 10.7 KB

README.adoc

File metadata and controls

241 lines (190 loc) · 10.7 KB

State Charts for Clojure(script)

An implementation of state charts that use the SCXML structure and semantics (as far as they are defined), without the need for XML.

statecharts CircleCI

See the official documentation (raw).

Production quality and API stable. Some obscure elements may not yet be implemented, but it is heavy use in a production environment without any issues.

  • Implemented

    • DataModel, ExecutionModel, and EventQueue abstractions, along with a simple implementation.

      • Late and early data model binding.

    • Compound, parallel, atomic, initial, and final states

      • on-entry, on-exit

      • History (deep and shallow). Multiple nodes allowed, with default transitions (for when there is no history).

    • Transitions

      • Eventless transitions

      • Guards

      • Executable content when taking a transition

    • Executable content elements:

      • Script, raise, send, log, assign, and done-data.

      • Executable content elements are extensible (you can make your own kind)

    • Simple dev-mode SCXML → CLJC conversion utility (see src/dev).

    • Invoke (services and other machines). You can add custom invocation handlers, but there are provided implementations for invoking other charts, and CLJ futures.

    • Send events to arbitrary other sessions/services.

    • A core.async event loop

  • Integration support for Fulcro.

  • Not implemented

    • SCXML Flow control executable content elements (if/else/elseif/foreach). Note: the executable content is extensible via multi-methods, so these are easy to add.

    • Complete test suite.

This library has a number of minor differences from the actual SCXML standard, and primarily is concerned with using the semantics and algorithm defined there. The concept of Event I/O Processors is merged into the general idea of an EventQueue protocol (that can accomplish all of the tasks of the aforementioned), and the "external event queue" for live sessions also uses this EventQueue as the source of external events. Thus, in this library each session has a true internal queue (which is only used internally, and is empty between calls to process-event!), but the "external event queue" is the same artifact as an Event I/O Processer.

Implementations of that protocol can easily support the mappings described in the standard if they want to, and event act as a manifold for distributing such events to different sub-processors (queues).

See Conformance.adoc at the root of this repository for other notes on decisions that affect exact conformance to the standard.

This library supports a lot of flexibility, and has many long-term goals:

  • Compatible with distributed systems. For example a cluster might run long-lived state machine sessions that can be serviced by any node of the custer.

  • State machine definitions can be versioned.

  • Everything can be made to be serializable (including the code in the machine).

Thus you can define (via protocols):

  • How to interpret the executable code of the state machine (e.g. on-exit, cond, etc.). You could, for example, use strings for those, and Javascript or SCI to interpret them (making the machine completely serializable).

  • How the data model of the states work. You can, for example, hook the data model into Fulcro, Reagent, Datomic, Datascript, etc.

  • The implementation of the event queue. This allows you to generate state machines that know how to send events to each other, can be distributed, etc. It is necessary to make this part of the internals because the state charts support sending events from within state code, which as we mentioned above, could be interpreted by your own execution model and may not actually be Clojure(script).

  • The actual state chart processing semantics/algorithm. A version of the W3C 2015-09-01 recommendation for SCXML is provided as a default.

  • How statechart working memory is stored over time (and space).

  • How statechart definitions are stored and retrieved.

However, it would be a real hassle if you had to set up all of that stuff just to get what many people want: A statechart that can run in CLJC, using Clojure code, and a functional interface where you can manually send events to the machine. Thus, a reasonable default implementation is provided for the above, and it is simple to expand this to meet your needs.

This library’s internal implementation follows (as closely as possible) the official State Chart XML Algorithm. In fact, much of the implementation uses internal volatiles in order to match the imperative style of that doc for easier comparison and avoidance of bugs.

The actual structure of the live CLJC data used to represent machines also closely mimics the structure described there, but with some differences for convenient use in CLJC.

Specifically, executable content is still treated as data, but the XML nodes that are described in the standard do not all exist in this library, because a conformant XML reader (which would need to be aware of the target execution model) can easily translate such nodes into the target data representation (even if that target representation is script strings).

Some of the data model elements are also abbreviated in a similar manner. See the docstrings for details.

Thus, if you are trying to read SCXML documents you will need to write (or find) an XML reader that can do this interpretation.

For example, an XML reader that targets sci (the Clojure interpreter) might convert the XML (where a and do-something are implied values in the data and excution model):

<if cond="(= 1 a)">
  (let [b (inc a)]
    (do-something b))
</if>

into (scope and args still determined by the execution model selected):

;; String-based interpretation
(script {:expr
  "(if (= 1 a)
     (let [b (inc a)]
       (do-something b)))"})

;; OR eval-based
(script {:expr
  '(if (= 1 a)
     (let [b (inc a)]
       (do-something b)))})

;; OR functional
(script {:expr (fn [env {:keys [a]}]
                  (if (= 1 a)
                    (let [b (inc a)]
                      (do-something b))))})

If you’re using XML tools to generate you machines, though, it’s probably easiest to use script tags to begin with.

The primary alternative to this library is clj-statecharts, which is a fine library modelled after xstate.

This library exists for the following reasons:

  • At the time this library was created, clj-statecharts was missing features. In particular history nodes, which we needed. I looked at clj-statecharts in order to try to add history, but some of the internal decisions made it more difficult to add (with correct semantics) and the Eclipse license made it less appealing for internal customization as a base in commercial software (see https://www.juxt.pro/blog/prefer-mit).

  • To create an SCXML-like implementation that uses the algorithm defined in the W3C Recommended document, and can (grow to) run (with minor transformations) SCXML docs that are targeted to Clojure with the semantics defined there (such as they are).

  • To define more refined abstract mechanisms such that the state charts can be associated to long-lived things (such as a monetary transaction that happens over time) and be customized to interface with things like durable queues for events (e.g. AWS SQS) and reliable timers.

  • MIT licensing instead of Eclipse.

Other related libraries and implementations:

  • XState : Javascript. Could be used from CLJS.

  • Apache SCXML : Stateful and imperative. Requires writing classes. Requires you use XML.

  • Fulcro UI State Machines : A finite state machine namespace (part of Fulcro) that is tightly coupled to Fulcro’s needs (full stack operation in the context of Fulcro UI and I/O).

This library was written using the reference implementation described in the SCXML standard, but without the requirement that the machine be written in XML.

Any deviation from the standard (as far as general operation of state transitions, order of execution of entry/exit, etc.) should be considered a bug. Note that it is possible for a bugfix in this library to change the behavior of your code (if you wrote it in a way that depends on the misbehavior); therefore, even though this library does not intend to make breaking changes, it is possible that a bugfix could affect your code’s operation.

If future versions of the standard are released that cause incompatible changes, then this library will add a new namespace for that new standard (not break versioning).

MIT License

Copyright (c) 2021 Fulcrologic

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.