Code Monkey home page Code Monkey logo

clj-statecharts's People

Contributors

doughamil avatar lucywang000 avatar mainej avatar olivergeorge avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

clj-statecharts's Issues

Making actions aware of the service that wraps the FSM

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?

Extensibility through data

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.

Desired outcomes

  1. Ability to use data (keywords, vectors) instead of functions, where applicable and practically possible
  2. Ability to inject additional context into functions on entry, guard, etc.

1. Less functions, more data

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:

  • Using a registry, referring to functions by keyword.
  • Supporting different value types for things like 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.

2. Injecting context

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.

Clarify EventLess Transitions documentation

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:

  • What happens if we are in state :s2 and event :e23 happens?
  • What happens if we are in state :s2 and another event (that is not :e23) happens?

Support for History states

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.

What signals does the machine accept given the current 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"}}}}

Docstrings not available on the public vars

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)

`assign` result is merged into state, instead of applied

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.

Storing :guard context in the machine

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.

Circular dependency between machine and scheduler

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.

Allow extra keys on 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.

Allow cancelling of `:delay`

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}}}}```

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.