Skip to content

Instantly share code, notes, and snippets.

@aamedina
Last active December 30, 2015 20:29
Show Gist options
  • Select an option

  • Save aamedina/7881544 to your computer and use it in GitHub Desktop.

Select an option

Save aamedina/7881544 to your computer and use it in GitHub Desktop.
;; the runtime is centered around three fundamental queues
;; 1) the input queue - your app sends messages to the input queue which uniquely identify a piece of state to update according
;; to the message params. These parameters are specified as part of the function definition, so there's no need to search around
;; for "configs" or "spec maps" anywhere in the code - app behavior is specified in the functions themselves.
;; 2) the output queue - this queue gets all messages that need to be processed outside of the application runtime -
;; common side effects that are relayed through this queue include DOM manipulation and Ajax requests
;; 3) the app model queue - this queue propagates renderable changes in the data model to the view (the actual frontend)
;; through the use of fine grained messages, called rendering deltas
;; test messages
(def default-msg
{:type :default :path [:nil :**] :value "hallo"})
(def swap-msg
{:type :swap :path [:other-counters "abc"] :value 42})
(def inc-msg
{:type :inc :path [:my-counter]})
;; test transforms
;; multimethod syntax is as follows
;; (defmethod multimethod-name dispatch-val arguments & function-body)
;; the dispatch-val is dispatched according to an arbitrary function defined with the methods corresponding defmulti -
;; this is a backend/non-application dev concern, so it's not directly applicable for dev user facing code to think about.
;; `(defmulti transform (fn [state message] [(:type message) (:path message)])`
;; from this definition you can see that transforms are dispatched based on the type and path of the input message
;; this method will accept messages with {:type :inc, :path [:my-counter]}
(defmethod transform [:inc [:my-counter]]
[state _] ;; _ is an idiom in clojure to denote arguments we don't care to give local bindings
((fnil inc 0) state))
;; fnil is a function `(fnil function default-value)` that returns a default value when the input is nil
;; we don't care about the state here, so now the state is _ and we take :value from the message
;; note the use of the wildcard :** - this transform will be called for any route with :type :swap
(defmethod transform [:swap [:**]]
[_ message]
(:value message))
;; derives are special, they take an input of paths to match against and create a new route at the output-path
;; syntax: [input-paths, output-path, input-spec - namely, how do you want the aggregated input to be specified,
;; :vals returns a sequence of the values, which in this case we're reducing
(defmethod derives [#{[:my-counter]
[:other-counters :*]} [:total-count] :vals]
[message state nums]
(reduce + nums))
(defmethod derives [#{[:my-counter]
[:other-counters :*]} [:max-count] :vals]
[message old-value nums]
(apply max (or old-value 0) nums))
(defmethod derives [{[:my-counter] :nums
[:other-counters :*] :nums
[:total-count] :total} [:average-count] :map]
[message old-value {:keys [nums total]}]
(/ total (count nums)))
;; this is an example of post-processing method, which is called after the data-model has been updated, but before the DOM
;; has been updated. Here we make sure that the :value being rendered to the DOM for the [:average-count] path is
;; rounded properly. This has all sorts of nifty uses - like formatting dates the way you want, currency, languages, etc.
;; the possibilities are endless!
(defmethod post-process [:value [:average-count]]
[[op path n]]
(letfn [(round [n places]
(let [p (Math/pow 10 places)]
(/ (Math/round (* p n)) p)))]
[[op path (round n 2)]]))
;; incase you're wondering how dependencies are handled and resolved at runtime - look no further!
;; there is a runtime dependency graph maintained by the dataflow engine which correctly orders every relationship
;; between every node in the data-model.
;; this is the relational programming part of things -
;; here's what the run time dependency graph would look like for the aforementioned functions
(require [foundation.app.dependency :as d])
(def counter-dependencies
(-> (d/graph)
(d/depend [:my-counter] nil)
(d/depend [:other-counters :*] nil)
(d/depend [:total-count] [:my-counter])
(d/depend [:total-count] [:other-counters :*])
(d/depend [:max-count] [:my-counter])
(d/depend [:max-count] [:other-counters :*])
(d/depend [:average-count] [:my-counter])
(d/depend [:average-count] [:total-count])
(d/depend [:average-count] [:other-counters :*])))
(comment
;; imagine [:my-counter] is being rendered as a button in the DOM. When clicked, the message is dispatched and the data-model
;; is updated. How do we make sure we update -every single- node which depends on the value at :my-counter?
;; enter - elementary graph theory!
(d/transitive-dependents counter-dependencies [:my-counter])
#{[:total-count] [:average-count] [:max-count]}
;; so, one simple call to get the transitive dependents of any path - this is a list of paths which are going to need new values
;; so how do we derive new values for these? simple! calculate their transitive dependencies!
;; [:my-counter] has no dependencies, which means it is "root" value. It does not derive its value from anything else in the
;; data-model.
(d/transitive-dependencies counter-dependencies [:my-counter])
#{}
;; Average count on the other hand was one of the transitive dependents of [:my-counter].
;; so to calculate its new value, we just have to reduce the state of all of its dependencies!
;; complicated code made simple with knowledge - (reduce depends graph [:average-count])
(d/transitive-dependencies counter-dependencies [:average-count])
#{[:other-counters :*] [:total-count] [:my-counter]}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment