; Define a function to send events
(defn event [k v] nil)
; Define the data structure used to model a component
(defstruct component-s :name :handlers :state)
; Create a component by creating an instance of the data structure
(defn make-component [name handlers]
(struct component-s name handlers {}))
; Run a collection of components, start by sending an :init event to each component
(defn run-components [& comps]
(loop [components comps
events (atom [])
key :init
value nil]
(if (= key nil)
; Component processing has completed - no new events have been generated
'Done
; Components have not completed processing - events are still left to be processed
(let [updated ; Updated list of components
; Bind the event function to a closure which can update the vector of events
(binding [event (fn [k v] (swap! events concat (vector (list k v))))]
; Map the update function accross each component in parallel
(doall (pmap #(conj % (if ((:handlers %) key)
{:state (conj (:state %)
(((:handlers %) key) value (:state %)))}
{}))
components)))]
; Recursively process components
(recur updated ; Updated list of components
(atom (rest @events)) ; Remaining events
(ffirst @events) ; Next event type
(second (first @events))))))) ; Next event message
We can test this by creating and running some test components:
(run-components
(make-component "Comp1"
{:init (fn [v s]
(event :set-msg "Ho")
(event :print nil)
{:msg "Hi"})
:print (fn [v s]
(println (:msg s)))
:set-msg (fn [v s]
{:msg v})})
(make-component "Comp2"
{:init (fn [v s]
{:msg "Hello"})
:print (fn [v s]
(println (:msg s)))}))
Comp1 contains three event handlers – :init, :print and :set-msg. Comp2 only contains two event handlers – :init and :print.
:init is called to setup the components state and trigger any initial events. :print will print the contents of the states :msg slot to the terminal and :set-msg will change the :msg slot to the value of the events message.
Running this sample will output the following:
Ho Hello Done
It becomes more interesting if the components do a little more, for example:
(run-components
(make-component "Comp1"
{:init (fn [v s]
(event :print nil) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(event :set-msg "Ho")
(event :print nil)
{:msg "Hi"})
:print (fn [v s]
(println (:msg s)))
:set-msg (fn [v s]
{:msg v})})
(make-component "Comp2"
{:init (fn [v s]
{:msg "Hello"})
:print (fn [v s]
(println (:msg s)))}))
Running this will output:
Hi Hello Ho Hello Done
Note how the first event gets processed before the second one – ie, Comp1’s :msg has not yet been modified.
Of course, you can set up all sorts of crazy chains of events with many event types, complex logic and so on.
What would be really nice is a DSL designed for writing these component-based programs, since calling run-components and make-components manually is a little messy. Something like the following would be nice:
(program
(component "Comp1"
(on :init [v s]
(event :print nil)
(event :set-msg "Ho")
(event :print nil)
{:msg "Hi"})
(on :print [v s]
(println (:msg s)))
(on :set-msg [v s]
{:msg v}))
(component "Comp2"
(on :init [v s]
{:msg "Hello"})
(on :print [v s]
(println (:msg s))))))
Luckily, this is the kind of thing Lisp is good at and Clojure, being a Lisp dialect, has everything we need to make it happen:
(defmacro program [& components]
`(run-components ~@components))
(defmacro component [name & handlers]
`(make-component ~name (conj {} ~@handlers)))
(defmacro on [handler args & body]
{handler `(fn ~args ~@body)})
These macros transform our convenient little DSL into a set of calls to run-components and make-components. Success! We can now write fun little component-based programs, arranged in a flat hierarchy, using an anonymous (sender never knows who receives the events; receiver never knows where the events come from) event-based messaging system.