lucywang000 / clj-statecharts Goto Github PK
View Code? Open in Web Editor NEWState Machine and StateCharts for Clojure(Script)
Home Page: https://lucywang000.github.io/clj-statecharts/
License: Eclipse Public License 1.0
State Machine and StateCharts for Clojure(Script)
Home Page: https://lucywang000.github.io/clj-statecharts/
License: Eclipse Public License 1.0
I'm experimenting with clj-statecharts for the purpose of using it with LibGDX. A common pattern there is for a screen instance to react to an action of a user by setting the next screen for the game instance. In Java (at least, in the code that I've seen), they do it by passing the game instance all over the place and making every screen create an instance of the next screen, which I'm not a fan of.
An obvious solution is to create an FSM with a service where some states have corresponding screens set up in their :entry
action. Those screens then would ask the service to transition to the right next state.
However, there's no convenient way of letting the actions know about the service.
Every action has two arguments - state
and event
. Putting the instance of a service into state
at the user level is not possible without writing a custom service since the transition-opts
are set at service instantiation time, when the service itself is not ready. Putting the instance into event
is doable but cumbersome since it has to be done on every event transition.
Would it make sense to add a :_service
key to the state
map when running actions?
I am getting the following error when running the friends example from the docs:
Error: fsm :friends got unknown event :success-load when in state
This is a place holder for creating a visualizer like the one XState has.
I have a few suggestions on how to make clj-statecharts even more extensible and data oriented. I mostly work in re-frame, so my requirements comes from that field, but these probably apply to other areas equally well. I'll describe them in re-frame terms here.
entry
, guard
, etc.In the app I'm working on at my daily job, sending schema and FSMs over the wire to be shared across the stack is very common. Serializing functions is of course possible, malli can do that, but it's easier if we can avoid it. There are several techniques or APIs that could work:
entry
and guard
, so that they can be vectors or any type instead of functions. First thing that comes to mind is a re-frame event vector as action
. Could be solved using multimethods.In our malli code, we specifically solved serialization by referring to objects through keywords. Our cljc-implemented registry could then be injected just before you need it, on the server or on the client.
It would be very powerful if all functions in the FSM definition could have arbitrary context injected. Imagine the following invocation:
(fsm/transition machine s1 {:type :timer} {:ctx {:db current-re-frame-db})
Which would make a guard
fn receive the :ctx
value in its args.
You may agree or disagree that this particular thing is a good idea, but it really opens up a lot of possibilities. For example: if the app uses an interceptor to integrate the FSM into re-frame, you could use a guard
to make the FSM reactively transition based on client db changes.
The documentation on EventLess Transitions describes very well what happens when in state :s1
, event :e12
happens and either guard23
or guard24
return true. If they are not true, it just says "Otherwise it would stay in :s2
."
What exactly happens in this case? Will :always
be evaluated again when the next event arrives while the machine is still in :s2
? Specifically:
:s2
and event :e23
happens?:s2
and another event (that is not :e23
) happens?The statecharts website has an article on what are history states. From the article,
A history state is a pseudostate, meaning that a state machine can’t rest in a history state. When a transition that leads to a history state happens, the history state itself doesn’t become active, rather the “most recently visited state” becomes active. It is a way for a compound state to remember (when it exits) which state was active, so that if the compound state ever becomes active again, it can go back to the same active substate, instead of blindly following the initial transition.
Does clj-statecharts support history states? I had a need for this. My states were simple enough that I got by with adding a guarded "always" transition to the initial state that moved it to its sibling state.
XState has an api to ask which signals are allowed given the current state:
const { initialState } = lightMachine;
console.log(initialState.nextEvents);
// => ['TIMER', 'EMERGENCY']
This is really important as this information can be used to enable/disable & hide/show action buttons on uis based on their current availability.
I propose clj-statecharts to have a function of machine state -> State
, e.g.
(def fsm
(fsm/machine
{:id :nested
:initial :a
:on {:top {:target :a
:meta {:ui {:text "restart"}}}}
:states {:a {:on {:a {:target :a
:meta {:ui {:text "nada"}}}
:b {:target :b
:meta {:ui {:text "goto b"}}}}}
:b {:on {:c :c}}
:c {:on {:c :c}}}}))
(fsm/next fsm (fsm/initialize fsm))
;{:top {:target :a
; :meta {:ui {:text "restart"}}}
; :a {:target :a
; :meta {:ui {:text "nada"}}}
; :b {:target :b
; :meta {:ui {:text "goto b"}}}}
I noticed when looking at the implementation of assign
that there was a nice docstring for it, but that it gets lost when re-def
-ed in statecharts.core
. Even for the functions without docstrings my editor provides a nice arglist hint that is pretty useful, which is missing for the statecharts.core
vars.
I'm not sure if there's a way to automatically assign the docstrings to another var, but you can do it manually like this:
(def assign ^{:doc "Wrap a function into a context assignment function." :arglists '([f])} impl/assign)
I've got a state machine:
(defn validate [{:keys [username password]}]
(-> {}
(cond-> (empty? username) (assoc :username :required))
(cond-> (empty? password) (assoc :password :required))
(not-empty)))
(def simple-sign-in
(fsm/machine
{:id :simple-sign-in
:initial :ready
:states
{:ready {:on {:submit :validate}
:states
{:error {:exit (fsm/assign (fn [state _] (dissoc state :errors)))}}}
:validate {:entry (fsm/assign (fn [state event]
(let [errors (validate event)]
(cond-> state errors (assoc :errors errors)))))
:always [{:target [:> :ready :error]
:guard (fn [state _] (:errors state))}
{:target :submitting}]}
:submitting {:on {:submit-success {:target :ready
:actions (fn [_ _] (println "dispatching submit success"))}
:submit-failure {:target [:> :ready :error]
:actions (fn [_ _] (println "dispatching submit fail"))}}}}}))
(as-> (fsm/initialize simple-sign-in) $
(fsm/transition simple-sign-in $ {:type :submit :username "" :password ""} )
(fsm/transition simple-sign-in $ {:type :submit :username "a" :password "b"} ))
Instead of proceeding to :submitting
after the second transition (because we fixed the validation errors), we instead end up back in [:ready :error]
. The problem is in the :exit
action on the [:ready :error]
state. assign
takes the result of the and merges it into the existing state. This means that it is impossible to remove a key from the context.
There's an easy workaround, which is to add nil
as a value for that key, but I think it would work more intuitively if the assign
-ed function was instead applied to the existing context to produce the next state value.
I'm finding this library very useful, but I'm struggling with the best way to handle validation, as for a re-frame form.
Here's my setup:
(defn validate [{:keys [username password]}]
(-> {}
(cond-> (empty? username) (assoc :username :required))
(cond-> (empty? password) (assoc :password :required))
(not-empty)))
(def simple-sign-in
(fsm/machine
{:id :simple-sign-in
:initial :ready
:states
{:ready
{:on
{:submit [{:target [:> :ready :error]
:guard (fn [_ event] (validate event))
;; TODO: figure out how to not run `validate` twice?
:actions (fn [_ event] (println '(rf/dispatch [:form/error (validate event)])))}
{:target :submitting
:actions (fn [_ event] (println '(rf/dispatch [:form/submit event])))}]}
:states {:error {}}}
:submitting {:on {:submit-success {:target :ready
:actions (fn [_ _] (println '(rf/dispatch [:form/success])))}
:submit-failure {:target [:> :ready :error]
:actions (fn [_ event] (println '(rf/dispatch [:form/error event])))}}}}}))
(as-> (fsm/initialize simple-sign-in) $
(fsm/transition simple-sign-in $ {:type :submit :username "" :password ""} ))
In transitioning from :ready
(or [:> :ready :error]
), I'm guarding the transition to the :error
state by running the validation function. Unfortunately, the :guard
throws away its return value, so I have to run it again in the :actions
step.
Is there a way to pass the return value of the guard into the machine context? I tried messing about with assign
, but doesn't really work because I think you need to return the machine state, which would mean the :guard
always passes. Or maybe I'm just barking up the wrong tree, but I can't figure out how to do validation without validating twice.
would be good to know if the process is done. Currently just :parallel
is supported as :type
in the Schema + would be good to not to allow signals after :final
state has reached.
There's a circular dependency between a machine and a scheduler.
transition
is passed a machine, so in case the transition leads to delayed events, the machine has to carry the scheduler. But the scheduler needs to refer to the machine in its dispatch function. And in turn, that machine has to have a scheduler, so that the delayed events can schedule further delayed events.
Because of this dependency, it's tricky to construct machines that need schedulers. The work around is to create some sort of mutable reference to a machine. See for example, here, here, and here. This works, but isn't idiomatic Clojure.
One fix would be for the machine to pass itself to the delayed/schedule
function (and on to the dispatch function):
;; delay.cljc
(defprotocol IScheduler
(schedule [this machine state event delay])
;; ^-- new
(unschedule [this machine state event]))
;; ^-- new
;; impl.cljc (edited for brevity)
(defn- execute-internal-action
[{:as fsm :keys [scheduler]} state transition-event internal-action]
,,,
(cond
(= action :fsm/schedule-event)
(fsm.d/schedule scheduler fsm state event event-delay)
;; ^-- new
(= action :fsm/unschedule-event)
(fsm.d/unschedule scheduler fsm state event)))
;; ^-- new
With this relationship, you could assoc a scheduler onto a machine, and not have to go through any further steps to ensure that the scheduler had access to the machine.
Currently, on can't add custom keys to machine, the following fails on extra key:
(def machine
(fsm/machine
{:id :lights
:initial :red
:context nil
:states {:green {:description "Green Light" ;; fail
:on {:timer {:target :yellow}}}
:yellow {:on {:timer :red}}
:red {:on {:timer :green}}}
:on {:power-outage :red}}))
Proposal: either make the schemas always open for extension or option to make them.
The :after
keyword can be used to specify a delayed transition, and allows a function that returns a dynamic delay. Would it make sense to cancel the delay if the :delay
function returns nil
?
LIke this (example from the docs)
(defn calculate-backoff
"Exponential backoff, with a upper limit of 15 seconds. Stops retrying after 15 attemtps"
[state & _]
(when (> 15 (:retries state))
(-> (js/Math.pow 2 (:retries state))
(* 1000)
(min 15000))))
(defn update-retries [state & _]
(update state :retries inc))
;; Part of the machine definition
{:states
{:connecting {:entry try-connect
:on {:success-connect :connected}}
:disconnected {:entry (assign update-retries)
:after [{:delay calculate-backoff :target :connecting}]}
:connected {:on {:connection-closed :disconnected}}}}```
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.