ova

stateful arrays for clojure


Author: Chris Zheng  (z@caudate.me)
Library: v1.0.1
Date: 07 December 2013
Website: http://github.com/zcaudate/ova
Generated By: MidjeDoc


1   Installation

2   Motivation

3   Walkthrough

  3.1   Constructor
  3.2   Dereferencing
  3.3   Append / Insert / Concat
  3.4   Select
  3.5   Remove / Filter
  3.6   Sorting
  3.7   Manipulation
  3.8   Ova Watch
  3.9   Element Watch
    3.9.1   Element Change Watch
  3.10   Clojure Protocols

4   Indices Selection

  4.1   by index
  4.2   by value
  4.3   by predicate
  4.4   by sets (or)
  4.5   by vectors (and)
  4.6   accessing nested elements

5   API Reference

  5.1   Basics
    5.1.1   ova
    5.1.2   persistent!
    5.1.3   reinit!
    5.1.4   <<
  5.2   Clojure
  5.3   Query
    5.3.1   select
    5.3.2   selectv
    5.3.3   fn
  5.4   Array Operations
    5.4.1   append!
    5.4.2   concat!
    5.4.3   insert!
    5.4.4   empty!
    5.4.5   remove!
    5.4.6   filter!
    5.4.7   sort!
    5.4.8   reverse!
  5.5   Element Operations
    5.5.1   !!
    5.5.2   map!
    5.5.3   smap!
    5.5.4   map-indexed!
    5.5.5   smap-indexed!
    5.5.6   !>
  5.6   Element Watch
    5.6.1   get-elem-watch
    5.6.2   add-elem-watch
    5.6.3   remove-elem-watch
    5.6.4   add-elem-change-watch

6   Scoreboard Example

  6.1   Setup
  6.2   Simulation

7   End Notes


ova

stateful arrays for clojure


Author: Chris Zheng  (z@caudate.me)
Library: v1.0.1
Date: 07 December 2013
Website: http://github.com/zcaudate/ova
Generated By: MidjeDoc


1    Installation

Add to project.clj dependencies (use double quotes):

[im.chit/ova '1.0.1']

All functions are in the ova.core namespace.

(use 'ova.core)

2    Motivation

An ova represents a mutable array of elements. The question should really be asked: Why?

Biased Answer: Because it is the most fully featured and bestest mutable array.... EVER!

In all seriousness, ova has been designed especially for dealing with shared mutable state in multi-threaded applications. Clojure uses refs and atoms off the shelf to resolve this issue but left out methods to deal with arrays of shared elements. ova has been specifically designed for the following use case:

These type of situations are usally co-ordinated using a external cache store like redis. Ova is no where near as fully featured as these libraries. The actual ova datastructure is a ref containing a vector containing ref. The library comes with a whole bundle of goodies to deal with mutation:

The library has been abstracted out of cronj, a task scheduling library where it is used to track and manipulate shared state. The ova syntax abstracts away alot of clutter. An example of tracking state in a multi-threaded environment can be seen in a scoreboard example:

3    Walkthrough

The key to ova lies in the ease of manipulating the postions of elements within an array as well as updating the elements themselves. We begin by constructing and displaying an ova.

3.1    Constructor

(def ov
  (ova [{:val 1} {:val 2}
        {:val 3} {:val 4}]))

(-> ov class str)
=> "class ova.core.Ova"

3.2    Dereferencing

An ova is a ref of a vector of refs. They are dereferenced accordingly:

(mapv deref (deref ov))
=> [{:val 1} {:val 2}
    {:val 3} {:val 4}]

(<< ov)                     ;; Shorthand
=> [{:val 1} {:val 2}
    {:val 3} {:val 4}]

3.3    Append / Insert / Concat

Adding elements to the ova is very straight forward:

(<< (append! ov {:val 6}))         ;; Append
=> [{:val 1} {:val 2} {:val 3}
    {:val 4} {:val 6}]

(<< (insert! ov {:val 5} 4))       ;; Insert
=> [{:val 1} {:val 2} {:val 3}
    {:val 4} {:val 5} {:val 6}]

(<< (concat! ov [{:val 7}          ;; Concat
                 {:val 8}]))
=> [{:val 1} {:val 2} {:val 3}
    {:val 4} {:val 5} {:val 6}
    {:val 7} {:val 8}]

3.4    Select

Where ova really shines is in the mechanism by which elements are selected. There are abundant ways of selecting elements - by index, by sets, by vectors, by predicates and by lists. The specific mechanism will be described more clearly in later sections.

(select ov 0)                      ;; By Index
=> #{{:val 1}}

(select ov #{0 1})                 ;; By Set of Index
=> #{{:val 1} {:val 2}}

(select ov {:val 3})               ;; By Item
=> #{{:val 3}}

(select ov #{{:val 3} {:val 4}})   ;; By Set of Items
=> #{{:val 3} {:val 4}}

(select ov #(-> % :val even?))     ;; By Predicate
=> #{{:val 2} {:val 4}
     {:val 6} {:val 8}}

(select ov '(:val even?))          ;; By List
=> #{{:val 2} {:val 4}
     {:val 6} {:val 8}}

(select ov [:val 3])               ;; By Vector/Value
=> #{{:val 3}}

(select ov [:val #{1 2 3}])       ;; By Vector/Set
=> #{{:val 1} {:val 2} {:val 3}}

(select ov [:val '(< 4)])         ;; By Vector/List
=> #{{:val 1} {:val 2} {:val 3}}

(select ov [:val even?            ;; By Vector/Predicate/List
            :val '(> 4)])
=> #{{:val 6} {:val 8}}

3.5    Remove / Filter

remove! and filter! also use the same mechanism as select:

(<< (remove! ov 7))               ;; Index Notation
=> [{:val 1} {:val 2} {:val 3}
    {:val 4} {:val 5} {:val 6}
    {:val 7}]

(<< (filter! ov #{1 2 3 4 5 6}))  ;; Set Notation
=> [{:val 2} {:val 3} {:val 4}
    {:val 5} {:val 6} {:val 7}]

(<< (filter! ov [:val odd?]))     ;; Vector/Fn Notation
=> [{:val 3} {:val 5} {:val 7}]

(<< (remove! ov [:val '(> 3)]))   ;; List Notation
=> [{:val 3}]

3.6    Sorting

The sort! functions allows elements in the ova to be rearranged. The function becomes clearer to read with access and comparison defined seperately (last example).

(def ov (ova (map (fn [n] {:val n})
                  (range 8))))

(<< ov)
=> [{:val 0} {:val 1} {:val 2}
    {:val 3} {:val 4} {:val 5}
    {:val 6} {:val 7}]

(<< (sort! ov (fn [a b]          ;; Fn
                (> (:val a)
                   (:val b)))))
=> [{:val 7} {:val 6} {:val 5}
    {:val 4} {:val 3} {:val 2}
    {:val 1} {:val 0}]

(<< (sort! ov [:val] <))         ;; Accessor/Comparater
=> [{:val 0} {:val 1} {:val 2}
    {:val 3} {:val 4} {:val 5}
    {:val 6} {:val 7}]

3.7    Manipulation

Using the same mechanism as select, bulk update of elements within the ova can be performed in a succint manner:

(def ov (ova (map (fn [n] {:val n})
                  (range 4))))

(<< ov)
=> [{:val 0} {:val 1} {:val 2} {:val 3}]

(<< (map! ov update-in [:val] inc))        ;; map! updates all elements
=> [{:val 1} {:val 2} {:val 3} {:val 4}]

(<< (smap! ov [:val odd?]                  ;; update only odd elements
           update-in [:val] #(+ 10 %)))
=> [{:val 11} {:val 2} {:val 13} {:val 4}]

(<< (smap! ov 0 update-in                     ;; update element at index 0
        [:val] #(- % 10)))
=> [{:val 1} {:val 2} {:val 13} {:val 4}]

(<< (smap! ov [:val 13]                       ;; update element with :val of 13
        update-in [:val] #(- % 10)))
=> [{:val 1} {:val 2} {:val 3} {:val 4}]

(<< (smap! ov [:val even?]                    ;; assoc new data to even :vals
        assoc-in [:x :y :z] 10))
=> [{:val 1} {:val 2 :x {:y {:z 10}}}
    {:val 3} {:val 4 :x {:y {:z 10}}}]

(<< (smap! ov [:x :y :z] dissoc :x))          ;; dissoc :x for elements with nested [:x :y :z] keys
=> [{:val 1} {:val 2} {:val 3} {:val 4}]

3.8    Ova Watch

Because a ova is simply a ref, it can be watched for changes

(def ov (ova [0 1 2 3 4 5]))

(def output (atom []))
(add-watch ov
           :old-new
           (fn [ov k p n]
             (swap! output conj [(mapv deref p)
                                 (mapv deref n)])))

(do (dosync (sort! ov >))
    (deref output))
=> [[[0 1 2 3 4 5]
     [5 4 3 2 1 0]]]

3.9    Element Watch

Entire elements of the ova can be watched. A more substantial example can be seen in the scoreboard example:

(def ov (ova [0 1 2 3 4 5]))

(def output (atom []))

(add-elem-watch      ;; key, ova, ref, previous, next
    ov :elem-old-new
    (fn [k o r p n]
      (swap! output conj [p n])))

(<< (!! ov 0 :zero))
=> [:zero 1 2 3 4 5]

(deref output)
=> [[0 :zero]]

(<< (!! ov 3 :three))
=> [:zero 1 2 :three 4 5]

(deref output)
=> [[0 :zero] [3 :three]]

3.9.1    Element Change Watch

The add-elem-change-watch function can be used to only notify when an element has changed.

(def ov (ova [0 1 2 3 4 5]))

(def output (atom []))

(add-elem-change-watch   ;; key, ova, ref, previous, next
   ov :elem-old-new  identity
   (fn [k o r p n]
     (swap! output conj [p n])))

(do (<< (!! ov 0 :zero))  ;; a pair is added to output
    (deref output))
=> [[0 :zero]]

(do (<< (!! ov 0 0))      ;; another pair is added to output
    (deref output))
=> [[0 :zero] [:zero 0]]

(do (<< (!! ov 0 0))      ;; no change to output
    (deref output))
=> [[0 :zero] [:zero 0]]

3.10    Clojure Protocols

ova implements the sequence protocol so it is compatible with all the bread and butter methods.

(def ov (ova (map (fn [n] {:val n})
                  (range 8))))

(seq ov)
=> '({:val 0} {:val 1} {:val 2}
     {:val 3} {:val 4} {:val 5}
     {:val 6} {:val 7})

(map #(update-in % [:val] inc) ov)
=> '({:val 1} {:val 2} {:val 3}
     {:val 4} {:val 5} {:val 6}
     {:val 7} {:val 8})

(last ov)
=> {:val 7}

(count ov)
=> 8

(get ov 0)
=> {:val 0}

(nth ov 3)
=> {:val 3}

(ov 0)
=> {:val 0}

(ov [:val] #{1 2 3}) ;; Gets the first that matches
=> {:val 1}

4    Indices Selection

There are a number of ways elements in an ova can be selected. The library uses custom syntax to provide a shorthand for element selection. We use the function indices in order to give an examples of how searches can be expressed. Most of the functions like select, remove!, filter!, smap!, smap-indexed!, and convenience macros are all built on top of the indices function and so can be used accordingly once the convention is understood.

4.1    by index

The most straight-forward being the index itself, represented using a number.

(def ov (ova [{:v 0, :a {:c 4}}    ;; 0
              {:v 1, :a {:d 3}}    ;; 1
              {:v 2, :b {:c 2}}    ;; 2
              {:v 3, :b {:d 1}}])) ;; 3

(indices ov)           ;; return all indices
=> [0 1 2 3]

(indices ov 0)         ;; return indices of the 0th element
=> [0]

(indices ov 10)        ;; return indices of the 10th element
=> []

4.2    by value

A less common way is to search for indices by value.

(indices ov            ;; return indices of elements matching term
         {:v 0 :a {:c 4}})
=> [0]

4.3    by predicate

Most of the time, predicates are used. They allow selection of any element returning a non-nil value when evaluated against the predicate. Predicates can take the form of functions, keywords or list representation.

(indices ov #(get % :a))   ;; retur indicies where (:a elem) is non-nil

=> [0 1]

(indices ov #(:a %))       ;; more succint function form

=> [0 1]

(indices ov :a)            ;; keyword form, same as #(:a %)

=> [0 1]

(indices ov '(get :a))     ;; list form, same as #(get % :a)

=> [0 1]

(indices ov '(:a))         ;; list form, same as #(:a %)

=> [0 1]

4.4    by sets (or)

sets can be used to compose more complex searches by acting as an union operator over its members

(indices ov #{0 1})        ;; return indices 0 and 1
=> [0 1]

(indices ov #{:a 2})       ;; return indices of searching for both 2 and :a
=> (just [0 1 2] :in-any-order)

(indices ov #{'(:a)        ;; a more complex example
              #(= (:v %) 2)})
=> (just [0 1 2] :in-any-order)

4.5    by vectors (and)

vectors can be used to combine predicates for more selective filtering of elements

(indices ov [:v 0])        ;; return indicies where (:a ele) = {:c 4}
=> [0]

(indices ov [:v '(= 0)])   ;; return indicies where (:a ele) = {:c 4}
=> [0]

(indices ov [:a #(% :c)])  ;; return indicies where (:a ele) has a :c element
=> [0]

(indices ov [:a '(:c)])    ;; with list predicate
=> [0]

(indices ov [:a :c])       ;; with keyword predicate
=> [0]

(indices ov [:v odd?       ;; combining predicates
             :v '(> 1)])
=> [3]

(indices ov #{[:a :c] 2})  ;; used within a set

=> (just [0 2] :in-any-order)

4.6    accessing nested elements

When dealing with nested maps, a vector can be used instead of a keyword to specify rules of selection using nested elements

(indices ov [[:b :c] 2])   ;; with value
=> [2]

(indices ov [[:v] '(< 3)]) ;; with predicate
=> [0 1 2]

(indices ov [:v 2          ;; combining in vector
             [:b :c] 2])
=> [2]

5    API Reference

5.1    Basics

5.1.1    ova

An ova deals with data in a vector. The data can be anything but it is recommended that the data are clojure maps.

(def ov (ova [1 2 3 4]))

(def ov (ova [{:id :a1 :score 10 :name "Bill"  :gender :m :nationality :aus}
               {:id :a2 :score 15 :name "John"  :gender :m :nationality :aus}]))

(def ov (ova [{:type "form" :data {:sex :m :age 23}}
               {:type "form" :data {:sex :f :age 24}}]))

5.1.2    persistent!

Since ova.core.Ova implements the clojure.lang.ITransientCollection interface, it can be made persistent with persistent!.

(persistent! (ova [1 2 3 4]))
=> [1 2 3 4]

5.1.3    reinit!

reinit! resets the data elements in an ova to another set of values. Any change in the ova requires it to be wrapped in a dosync macro.

(def ov (ova [1 2 3 4]))
(dosync (reinit! ov [5 6 7 8 9]))
(persistent! ov)
=> [5 6 7 8 9]

5.1.4    <<

The output macro is a shorthand for outputting the value of ova after a series of transformations. There is an implicit dosync block within the macro.

(<< (def ov (ova [1 2 3 4]))
    (reinit! ov [5 6 7 8 9]))
=> [5 6 7 8 9]

5.2    Clojure

Built-in operations supported including (but not limited to):

5.3    Query

Where ova shines is in the various ways that elements can be selected. It is best to define some data that can be queried:

(def players
  (ova [{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}
        {:id :a2 :score 15 :info {:name "John"  :gender :m :nationality :aus}}
        {:id :a3 :score 15 :info {:name "Dave"  :gender :m :nationality :aus}}
        {:id :a4 :score 11 :info {:name "Henry" :gender :m :nationality :usa}}
        {:id :a5 :score 20 :info {:name "Scott" :gender :m :nationality :usa}}
        {:id :a6 :score 13 :info {:name "Tom"   :gender :m :nationality :usa}}
        {:id :a7 :score 15 :info {:name "Jill"  :gender :f :nationality :aus}}
        {:id :a8 :score 19 :info {:name "Sally" :gender :f :nationality :usa}}
        {:id :a9 :score 13 :info {:name "Rose"  :gender :f :nationality :aus}}]))

5.3.1    select

Index:
(select players 0)
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}}
Predicates:
(select players #(= (:id %) :a1))
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}}
List Predicates:
(select players '(:id (= :a1)))
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}}
Vector Predicates:
(select players [:id :a1])
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}}

(select players [:score even?])
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}
     {:id :a5 :score 20 :info {:name "Scott" :gender :m :nationality :usa}}}

(select players [:score '(< 13)])
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}
     {:id :a4 :score 11 :info {:name "Henry" :gender :m :nationality :usa}}}

(select players [:score 13 [:info :gender] :f])
=> #{{:id :a9 :score 13 :info {:name "Rose"  :gender :f :nationality :aus}}}
Sets:
(select players #{1 2})
=> #{{:id :a2 :score 15 :info {:name "John"  :gender :m :nationality :aus}}
     {:id :a3 :score 15 :info {:name "Dave"  :gender :m :nationality :aus}}}

(select players #{[:score even?] [:score 13 [:info :gender] :f]})
=> #{{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}
     {:id :a5 :score 20 :info {:name "Scott" :gender :m :nationality :usa}}
     {:id :a9 :score 13 :info {:name "Rose"  :gender :f :nationality :aus}}}

5.3.2    selectv

selectv is the same as select except it returns a vector instead of a set.

(selectv players #{[:score even?] [:score 13 [:info :gender] :f]})
=> (just [{:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}
          {:id :a5 :score 20 :info {:name "Scott" :gender :m :nationality :usa}}
          {:id :a9 :score 13 :info {:name "Rose"  :gender :f :nationality :aus}}]
         :in-any-order)

5.3.3    fn

ova implements the clojure.lang.IFn interface and so can be called with select parameters. It can be used to return elements within an array. Additionally, if an element has an :id tag, it will search based on the :id tag.

(players 0)
=> {:id :a1 :score 10 :info {:name "Bill"  :gender :m :nationality :aus}}

(players 1)
=> {:id :a2 :score 15 :info {:name "John"  :gender :m :nationality :aus}}

(players :a3)
=> {:id :a3 :score 15 :info {:name "Dave"  :gender :m :nationality :aus}}

(:a3 players)
=> {:id :a3 :score 15 :info {:name "Dave"  :gender :m :nationality :aus}}

(ov :a10)
=> nil

5.4    Array Operations

5.4.1    append!

append! adds additional elements to the end:

(<< (append! (ova [1 2 3 4])
             5 6 7 8))
=> [1 2 3 4 5 6 7 8]

5.4.2    concat!

concat! joins an array at the end:

(<< (concat! (ova [1 2 3 4])
             [5 6 7 8]))
=> [1 2 3 4 5 6 7 8]

5.4.3    insert!

insert! allows elements to be inserted.

(<< (insert! (ova [:a :b :c :e :f])
             :d 3))
 => [:a :b :c :d :e :f]

5.4.4    empty!

empty! clears all elements

(<< (empty! (ova [:a :b :c :d])))
=> []

5.4.5    remove!

remove! will selectively remove elements from the ova. The query syntax can be used

(<< (remove! (ova [:a :b :c :d])
             '(= :a)))
=> [:b :c :d]

(<< (remove! (ova [1 2 3 4 5 6 7 8 9])
             #{'(< 3) '(> 6)}))
=> [3 4 5 6]

5.4.6    filter!

filter! performs the opposite of remove!. It will keep all elements in the array that matches the query.

(<< (filter! (ova [:a :b :c :d])
             '(= :a)))
=> [:a]

(<< (filter! (ova [1 2 3 4 5 6 7 8 9])
           #{'(< 3) '(> 6)}))
=> [1 2 7 8 9]

5.4.7    sort!

sort! arranges the array in order of the comparator. It can take only a comparator, or a selector/comparator combination.

(<< (sort! (ova [9 8 7 6 5 4 3 2 1])
           <))
=> [1 2 3 4 5 6 7 8 9]

(<< (sort! (ova [1 2 3 4 5 6 7 8 9])
           identity >))
=> [9 8 7 6 5 4 3 2 1]

5.4.8    reverse!

reverse! arranges array elements in reverse

(<< (reverse! (ova [1 2 3 4 5 6 7 8 9])))
=> [9 8 7 6 5 4 3 2 1]

5.5    Element Operations

Element operations are specific to manipulating the elements within the array.

5.5.1    !!

!! sets the value of all selected indices to a specified value.

(<< (!! (ova [1 2 3 4 5 6 7 8 9]) 0 0))
=> [0 2 3 4 5 6 7 8 9]


(<< (!! (ova [1 2 3 4 5 6 7 8 9]) odd? 0))
=> [0 2 0 4 0 6 0 8 0]


(<< (!! (ova [1 2 3 4 5 6 7 8 9]) '(> 4) 0))
=> [1 2 3 4 0 0 0 0 0]

5.5.2    map!

map! performs an operation on every element.

(<< (map! (ova [1 2 3 4 5 6 7 8 9])
          inc))
=> [2 3 4 5 6 7 8 9 10]

5.5.3    smap!

smap! performs an operation only on selected elements

(<< (smap! (ova [1 2 3 4 5 6 7 8 9])
           odd? inc))
=> [2 2 4 4 6 6 8 8 10]

5.5.4    map-indexed!

map-indexed! performs an operation with the element index as the second parameter on every element

(<< (map-indexed! (ova [1 2 3 4 5 6 7 8 9])
                  +))
=> [1 3 5 7 9 11 13 15 17]

5.5.5    smap-indexed!

smap-indexed! performs an operation with the element index as the second parameter on selected elements

(<< (smap-indexed! (ova [1 2 3 4 5 6 7 8 9])
                   odd? +))
=> [1 2 5 4 9 6 13 8 17]

5.5.6    !>

The threading array performs a series of operations on selected elements.

(<< (!> (ova [1 2 3 4 5 6 7 8 9])
        odd?
        (* 10)
        (+ 5)))
=> [15 2 35 4 55 6 75 8 95]

5.6    Element Watch

Watches can be set up so that. Instead of a normal ref/atom watch where there are four inputs to the watch function, the Element watch requires an additional input to distinguish which array a change has occured. The function signature looks like:

(fn [k o r p v]  ;; key, ova, ref, prev, current
  (... do something ...))

5.6.1    get-elem-watch

get-elem-watches takes as input an ova and returns a map of element watches and their keys.

5.6.2    add-elem-watch

add-elem-watch adds a watch function on all elements of an ova.

(def ov     (ova [1 2 3 4]))
(def watch  (atom []))
(def cj-fn  (fn  [k o r p v]  ;; key, ova, ref, prev, current
              (swap! watch conj [p v])))

(add-elem-watch ov :conj cj-fn) ;; add watch
(keys (get-elem-watches ov))    ;; get watches
=> [:conj]

(<< (map! ov + 10))   ;; elements in ov are manipulated
=> [11 12 13 14]

(sort @watch)
=> [[1 11] [2 12] [3 13] [4 14]]

5.6.3    remove-elem-watch

remove-elem-watch cleares the element watch function to a ova.

(remove-elem-watch ov :conj)
(keys (get-elem-watches ov))
=> nil

5.6.4    add-elem-change-watch

add-elem-change-watch only updates when part of the array changes. This is a really useful abstraction when the element is a big nested map. This is the same as add-elem-watch though an additional selector is needed to determine if the expected part of the element has change. Its usage can be seen in the example

6    Scoreboard Example

6.1    Setup

data

(def scoreboard
  (ova [{:name "Bill" :attempts 0 :score {:all ()}}
        {:name "John" :attempts 0 :score {:all ()}}
        {:name "Sally" :attempts 0 :score {:all ()}}
        {:name "Fred"  :attempts 0 :score {:all ()}}]))

update notifiers

(add-elem-change-watch
   scoreboard :notify-attempt [:attempts]
   (fn [k o r p n]  ;; key, ova, ref, previous, next
     (println (:name @r) "is on attempt" n)))

(add-elem-change-watch
   scoreboard :notify-high-score [:score :highest]
   (fn [k o r p n]
     (println (:name @r) "has a new highscore: " n)))

score watch

(add-elem-change-watch
   scoreboard :update-high-score [:score :all]
   (fn [k o r p n]
     (let [hs    [:score :highest]
           high  (get-in @r hs)
           current (first n)]
       (if (and current
                (or (nil? high)
                    (< high current)))
         (dosync (alter r assoc-in hs current))))))

game simulation

(defn sim-game [scoreboard name]
  ;; increment number of attempts
  (dosync (!> scoreboard [:name name]
              (update-in [:attempts] inc)))

  ;; simulate game playing time
  (Thread/sleep (rand-int 500))

  ;; conj the newest score at the start of the list
  (dosync (!> scoreboard [:name name]
              (update-in [:score :all] conj (rand-int 50)))))

(defn sim-n-games [scoreboard name n]
  (when (> n 0)
    (Thread/sleep (rand-int 500))
    (sim-game scoreboard name)
    (recur scoreboard name (dec n))))

multi-threading

(defn sim! [scoreboard]
  (let [names (map :name scoreboard)]
    (doseq [nm names]
      (future (sim-n-games scoreboard nm (+ 5 (rand-int 5)))))))

6.2    Simulation

A sample simulation is show below:

(sim! scoreboard)

=> [Sally is on attempt 1
    Bill is on attempt 1
    Bill has a new highscore  35
    Sally has a new highscore  40
    John is on attempt 1
    Fred is on attempt 1
    Sally is on attempt 2
    Fred has a new highscore  38
    John has a new highscore  28
    Bill is on attempt 2
    Fred is on attempt 2
    John is on attempt 2
    John is on attempt 3
    Bill is on attempt 3
    Sally is on attempt 3
    Bill has a new highscore  36
    Bill is on attempt 4
    John is on attempt 4
    Fred is on attempt 3
    Sally is on attempt 4
    Fred is on attempt 4
    John is on attempt 5
    Sally is on attempt 5
    Bill is on attempt 5
    Bill has a new highscore  39
    Bill is on attempt 6
    John has a new highscore  30
    Fred is on attempt 5
    John is on attempt 6
    Bill has a new highscore  41
    John is on attempt 7
    Bill is on attempt 7
    Sally is on attempt 6
    John is on attempt 8
    Sally is on attempt 7
    Bill is on attempt 8
    John is on attempt 9
    John has a new highscore  39
    Sally is on attempt 8
    Bill has a new highscore  45
    Sally is on attempt 1
    Fred is on attempt 1
    John is on attempt 1
    Bill is on attempt 1
    Sally has a new highscore  49
    John has a new highscore  49
    Bill has a new highscore  3
    Bill is on attempt 2
    Fred has a new highscore  22
    John is on attempt 2
    Bill has a new highscore  18
    Fred is on attempt 2
    Bill is on attempt 3
    Sally is on attempt 2
    John is on attempt 3
    Fred is on attempt 3
    Bill has a new highscore  39
    Fred has a new highscore  47
    Fred is on attempt 4
    John is on attempt 4
    Bill is on attempt 4
    Sally is on attempt 3
    Fred is on attempt 5
    Sally is on attempt 4
    John is on attempt 5
    Bill is on attempt 5
    Sally is on attempt 5
    Bill is on attempt 6
    John is on attempt 6
    Sally is on attempt 6
    Sally is on attempt 7
    John is on attempt 7
    Bill is on attempt 7
    Bill is on attempt 8
    Sally is on attempt 8
    Bill has a new highscore  44
    Bill is on attempt 9
    Bill has a new highscore  45]

(<< scoreboard)

=> [{:name "Bill", :attempts 9, :score {:highest 45, :all (45 44 36 9 24 25 39 18 3)}}
    {:name "John", :attempts 7, :score {:highest 49, :all (20 37 32 8 48 37 49)}}
    {:name "Sally", :attempts 8, :score {:highest 49, :all (1 48 7 12 43 0 39 49)}}
    {:name "Fred", :attempts 5, :score {:highest 47, :all (16 40 47 15 22)}}]

7    End Notes

For any feedback, requests and comments, please feel free to lodge an issue on github or contact me directly.

Chris.