Skip to content

Instantly share code, notes, and snippets.

@ordnungswidrig
Created June 16, 2011 15:50
Show Gist options
  • Save ordnungswidrig/1029550 to your computer and use it in GitHub Desktop.
Save ordnungswidrig/1029550 to your computer and use it in GitHub Desktop.
State is a fold over events
(ns state-is-a-fold
(:use clojure.test))
;;; After all, state is a fold of events. For example let's say the events are a sequence of numbers
;;; and we are folding by addition:
(deftest simple
(let [events [1 5 2 4 3]
state (reduce + events)]
(is (= 15 state))))
;;; We can use this idea to do a little event handling. First, we define a protocol for everybody
;;; who is interested in a event. we will use the function handle-event to folder the sequence of
;;; events over an initial state of the handler.
(defprotocol EventHandler
(handle-event
;; Apply the event on the handler and return a handler who reflects the consumation of the event.
;; Returning the same instance is fine, e.g. when the handler ignores the event or the processing
;; consists of a side effect only.
[this event]))
;; For example a event counter
(defrecord EventCounter [i]
EventHandler
(handle-event [_ _] (EventCounter. (inc i))))
;; Now we can calculate state as a fold over events:
(defn fold-events [initial events]
(reduce handle-event initial events))
(deftest test-fold--events []
(let [initial (EventCounter. 0)
events (range 10)
state (fold-events initial events)]
(is (= 10 (:i state)))))
;; As an illustration we define a side-effect-only event hanlder
(defrecord SideEffectHandler [ref]
EventHandler
(handle-event [this _] (alter ref inc) this))
(deftest test-side-effects-only
(let [initial (SideEffectHandler. (ref 0))
events (range 10)
state (dosync (fold-events initial events))]
(is (identical? initial state))
(is (= 10 (-> initial :ref deref)))))
;; In this case we could also use reify and avoid defining a record:
(defn make-event-printer []
(reify EventHandler
(handle-event [ _ e] (println e))))
;; Some handlers will only act on certain events:
(defrecord Adder [sum]
EventHandler
(handle-event
[this event]
(if (= (:type event) :number)
(Adder. (+ sum (:value event)))
this)))
;; Some will keep a little more state
(defrecord Averager [sum cnt avg]
EventHandler
(handle-event
[this event]
(if (= (:type event) :number)
(let [sum (+ sum (:value event))
cnt (inc cnt)]
(Averager. sum cnt (/ sum cnt)))
this)))
(defrecord MinMax [min_ max_] ;; avoid using field names that are core functions
EventHandler
(handle-event
[this {type :type number :value}]
(if (= type :number)
(MinMax. (min (or min_ number) number) (max (or max_ number) number))
this)))
;; By the way, testing state is easy with handlers:
(deftest test-min-max
(testing "nil handling"
(is (= (MinMax. 5 5)
(handle-event (MinMax. nil nil) {:type :number :value 5}))))
(testing "ignores other events"
(is (= (MinMax. 1 3)
(handle-event (MinMax. 1 3) {:type :foo})))))
;; I think you get the idea. An Eventhandler is nothing but a function with the
;; type signature
;;
;; f : EventHandler -> Event -> EventHandler.
;;
;; As you can see this matches perfectly the signature of foldl.
;;
;; foldl : ( a -> b -> a) -> a -> [b] -> a
;;
;; You can also think of the event stream as a sequence of functions over the
;; state. So we combine the event and the function to a state changing function.
;; The fold using f and the events becomes a simple function composition.
(defn fold-as-comp [f initial events]
((apply comp (map (fn [e] (partial f e)) events)) initial))
;; Now for some fun we compose handlers. The state of the composed handler will
;; evolve by mapping the handling function of the contained handlers.
(defrecord Composed [handlers]
EventHandler
(handle-event
[_ event]
(Composed.
(map #(handle-event % event) handlers))))
(defn compose-handlers [& handlers]
(Composed. handlers))
;; To do some real event sourcing we need an event recording. This implementation
;; uses an simple list and thus does not rely on side effects.
(defrecord Recorder [events]
EventHandler (handle-event [_ event] (Recorder. (conj events event))))
;; We can even keep the history of states:
(defrecord HistoryHandler [history]
EventHandler
(handle-event
[_ event]
(HistoryHandler. (conj history (handle-event (last history) event)))))
(defn wrap-history [handler]
(HistoryHandler. [handler]))
;; Let's mix this up for a little composition fun. Look how the event of type :foo
;; is ignored by the handlers Adder, Averager and MinMax.
(deftest test-state
(testing "composed handling"
(let [events [{:type :number :value 5}
{:type :number :value 4}
{:type :number :value 3}
{:type :foo :value 3}
{:type :number :value 2}
{:type :number :value 1}]
initial-state (compose-handlers
(Recorder. nil)
(Adder. 0)
(Averager. 0 0 0)
(MinMax. nil nil))
final-state (fold-events initial-state events)
[recorder adder averager minmax] (:handlers final-state)]
(testing "handler handle events"
(is (= 6 (count (:events recorder))))
(is (= 15 (:sum adder)))
(is (= 3 (:avg averager)))
(is (= 1 (:min_ minmax)))
(is (= 5 (:max_ minmax))))
(testing "state history"
(let [history (fold-events (wrap-history initial-state) events)]
(is (= 7 (count (:history history))))
(is (= initial-state (first (:history history))))
(doseq [[fs hs] (map vector
(:handlers final-state)
(:handlers (last (:history history))))]
(is (= fs hs))))))))
;; Trigger all tests:
(test-ns *ns*)
;; Where can we go from here? Next I'll try to implement persistence of events and snapshotting.
;; Enabling concurrency would be fine, too, e.g. by using references to store the handlers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment