Date: 27 November 2018
Version: 3.0.2
1 Introduction
An ova
represents a mutable array of elements. It 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:
- Elements (usually clojure maps) can be added or removed from an array
- Element data are accessible and mutated from several threads.
- Array itself can also be mutated from several threads.
2 General
Methods for setting up an ova and accessing it's data
<< ^
outputs outputs the entire output of an ova
(-> (ova [1 2 3 4 5])
(append! 6 7 8 9)
(<<))
=> [1 2 3 4 5 6 7 8 9]
;; can also use `persistent!`
(-> (ova [1 2 3 4 5])
(persistent!))
=> [1 2 3 4 5]
clone ^
creates an exact copy of the ova, including its watches
(def o (ova (range 10)))
(watch/set o {:a (fn [_ _ _ _ _])})
(def other (clone o))
(<< other) => (<< o)
(watch/list other) => (just {:a fn?})
has? ^
checks that the ova contains elements matching a selector
(def o (ova [{:id :1 :val 1} {:id :2 :val 1}
{:id :3 :val 2} {:id :4 :val 2}]))
(has? o)
=> true
(has? o 0)
=> true
(has? o -1)
=> false
(has? o [:id '((name)
(bigint)
(odd?))])
=> true
indices ^
instead of data, outputs the matching indices
(def o (ova [{:id :1 :val 1} {:id :2 :val 1}
{:id :3 :val 2} {:id :4 :val 2}]))
(indices o)
=> [0 1 2 3]
(indices o 0)
=> [0]
(indices o [:val 1])
=> [0 1]
(indices o [:val even?])
=> [2 3]
(indices o [:val even?
'(:id (name) (bigint)) odd?])
=> [2]
(indices o #{4})
=> []
(indices o [:id :1])
=> [0]
init! ^
sets elements within an ova
(def o (ova []))
(->> (init! o [{:id :1 :val 1} {:id :2 :val 1}])
(dosync)
(<<))
=> [{:val 1, :id :1} {:val 1, :id :2}]
ova ^
constructs an instance of an ova
(ova []) ;=> #ova []
(ova [1 2 3]) ;=> #ova [1 2 3]
(<< (ova [{:id :1} {:id :2}]))
=> [{:id :1} {:id :2}]
ova? ^
checks if an object is an ova instance
(ova? (ova [1 2 3]))
=> true
select ^
grabs the selected ova entries as a set of values
(def o (ova [{:id :1 :val 1} {:id :2 :val 1}
{:id :3 :val 2} {:id :4 :val 2}]))
(select o) ;; no filters
=> #{{:id :1, :val 1}
{:id :2, :val 1}
{:id :3, :val 2}
{:id :4, :val 2}}
(select o 0) ;; by index
=> #{{:id :1 :val 1}}
(select o #{1 2}) ;; by indices
=> #{{:id :2 :val 1}
{:id :3 :val 2}}
(select o #(even? (:val %))) ;; by function
=> #{{:id :3 :val 2}
{:id :4 :val 2}}
(select o [:val 1]) ;; by shorthand value
=> #{{:id :1 :val 1}
{:id :2 :val 1}}
(select o [:val even?]) ;; by shorthand function
=> #{{:id :3 :val 2}
{:id :4 :val 2}}
(select o #{[:id :1] ;; or selection
[:val 2]})
=> #{{:id :1 :val 1}
{:id :3 :val 2}
{:id :4 :val 2}}
(select o [:id '((name) ;; by shorthand expression
(bigint)
(odd?))])
=> #{{:id :1 :val 1}
{:id :3 :val 2}}
selectv ^
grabs the selected ova entries as vector
(def o (ova [{:id :1 :val 1} {:id :2 :val 1}
{:id :3 :val 2} {:id :4 :val 2}]))
(selectv o) ;; no filters
=> [{:id :1, :val 1}
{:id :2, :val 1}
{:id :3, :val 2}
{:id :4, :val 2}]
(selectv o 0) ;; by index
=> [{:id :1 :val 1}]
(selectv o [:val even?]) ;; by shorthand function
=> [{:id :3 :val 2}
{:id :4 :val 2}]
(selectv o [:id '((name) ;; by shorthand expression
(bigint)
(odd?))])
=> [{:id :1 :val 1}
{:id :3 :val 2}]
3 Operation
!! ^
sets the value of selected data cells in the ova
(-> (range 5)
(ova)
(!! 1 0)
(<<))
=> [0 0 2 3 4]
(-> (range 5)
(ova)
(!! #{1 2} 0)
(<<))
=> [0 0 0 3 4]
(-> (range 5)
(ova)
(!! even? 0)
(<<))
=> [0 1 0 3 0]
!> ^
applies a set of transformations to a selector on the ova
(<< (!> (ova [{:id :1}])
0
(assoc-in [:a :b] 1)
(update-in [:a :b] inc)
(assoc :c 3)))
=> [{:id :1 :c 3 :a {:b 2}}]
append! ^
like `conj!` but appends multiple array elements to the ova
(-> (ova [{:id :1 :val 1}])
(append! {:id :2 :val 1}
{:id :3 :val 2})
(<<))
=> [{:id :1 :val 1}
{:id :2 :val 1}
{:id :3 :val 2}]
concat! ^
works like `concat`, allows both array and ova inputs
(<< (concat! (ova [{:id :1 :val 1}
{:id :2 :val 1}])
(ova [{:id :3 :val 2}])
[{:id :4 :val 2}]))
=> [{:val 1, :id :1}
{:val 1, :id :2}
{:val 2, :id :3}
{:val 2, :id :4}]
empty! ^
empties an existing ova
(-> (ova [1 2 3 4 5])
(empty!)
(<<))
=> []
filter! ^
keep only elements that matches the selector
(-> (ova [0 1 2 3 4 5 6 7 8 9])
(filter! #{'(< 3) '(> 6)})
(<<))
=> [0 1 2 7 8 9]
insert! ^
inserts data at either the end of the ova or when given an index
(-> (ova (range 5))
(insert! 6)
(<<))
=> [0 1 2 3 4 6]
(-> (ova (range 5))
(insert! 6)
(insert! 5 5)
(<<))
=> [0 1 2 3 4 5 6]
map! ^
applies a function on the ova with relevent arguments
(-> (ova [{:id :1} {:id :2}])
(map! assoc :val 1)
(<<))
=> [{:val 1, :id :1}
{:val 1, :id :2}]
map-indexed! ^
applies a function that taking the data index as well as the data
to all elements of the ova
(-> (ova [{:id :1} {:id :2}])
(map-indexed! (fn [i m]
(assoc m :val i)))
(<<))
=> [{:val 0, :id :1}
{:val 1, :id :2}]
remove! ^
removes data from the ova that matches a selector
(-> (ova (range 10))
(remove! odd?)
(<<))
=> [0 2 4 6 8]
(-> (ova (range 10))
(remove! #{'(< 3) '(> 6)})
(<<))
=> [3 4 5 6]
reverse! ^
reverses the order of elements in the ova
(-> (ova (range 5))
(reverse!)
(<<))
=> [4 3 2 1 0]
smap! ^
applies a function to only selected elements of the array
(-> (ova [{:id :1 :val 1}
{:id :2 :val 1}
{:id :3 :val 2}
{:id :4 :val 2}])
(smap! [:val 1]
update-in [:val] #(+ % 100))
(<<))
=> [{:id :1, :val 101}
{:id :2, :val 101}
{:id :3, :val 2}
{:id :4, :val 2}]
smap-indexed! ^
applies a function that taking the data index as well as the data
to selected elements of the ova
(-> (ova [{:id :1 :val 1}
{:id :2 :val 1}
{:id :3 :val 2}
{:id :4 :val 2}])
(smap-indexed! [:val 1]
(fn [i m]
(update-in m [:val] #(+ i 100 %))))
(<<))
=> [{:id :1, :val 101}
{:id :2, :val 102}
{:id :3, :val 2}
{:id :4, :val 2}]
sort! ^
sorts all data in the ova using a comparator function
(-> (ova [2 1 3 4 0])
(sort! >)
(<<))
=> [4 3 2 1 0]
(-> (ova [2 1 3 4 0])
(sort! <)
(<<))
=> [0 1 2 3 4]
split ^
splits an ova into two based on a predicate
(def o (ova (range 10)))
(def sp (dosync (split o #{'(< 3) '(> 6)})))
(persistent! (sp true)) => [0 1 2 7 8 9]
(persistent! (sp false)) => [3 4 5 6]
4 Watches
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]]]
4.1 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 []))
(watch/add ;; 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]]
4.2 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 []))
(watch/add ;; key, ova, ref, previous, next
ov :elem-old-new
(fn [k o r p n]
(swap! output conj [p n]))
{:select identity
:diff true})
(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]]
4.3 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}
5 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.
5.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
=> []
5.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]
5.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]
5.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)
5.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)
5.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]
6 Scoreboard
6.1 Data Setup
A scoreboard is used to track player attempts, scores and high-scores
(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 ()}}]))
6.2 Notifier Setup
hara.event
is used to listen for a :log
signal and print out the :msg
component of the event.
(require '[hara.event :as event])
(event/deflistener print-logger
:log
[msg]
(println msg))
We set up two watch notifiers that signal and event.
- one to print when an attempt has been made to play a game
- one to print when there is a new highscore
(watch/add scoreboard
:notify-attempt
(fn [k o r p n] ;; key, ova, ref, previous, next
(event/signal [:log {:msg (str (:name @r) " is on attempt " n)}]))
{:select :attempts})
(watch/add scoreboard
:notify-high-score
(fn [k o r p n]
(event/signal [:log {:msg (str (:name @r) " has a new highscore of " n)}]))
{:select [:score :highest]})
Of course, we could have added the println
statement directly. However, in an actual application, events may be logged to file, emailed, beeped or read back to the user. Having a light-weight event signalling framework lets that decision be made much later
6.3 High Scores
Another watch is added to update the high score whenever it occurs.
(watch/add scoreboard
:update-high-score
(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)))))
{:select [:score :all]})
6.4 Game Simulation
Functions for simulation are defined with the following parameters:
sim-game
and sim-n-games
are used to update the scoreboard- the time to finish the game is randomised
- the wait-time between subsequent games is randomised
- the score they get is also randomised
(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))))
6.5 Multithreading
To demonstrate the use of ova within a multithreaded environment, we run the following simulation
- for each player on the scoreboard, they each play a random number of games simultaneously
- the same scoreboard is used to keep track of state
(defn sim! [scoreboard]
(let [names (map :name scoreboard)]
(doseq [nm names]
(future (sim-n-games scoreboard nm (+ 5 (rand-int 5)))))))
A sample simulation is show below:
(sim! scoreboard)
=> [Sally is on attempt 1
Bill is on attempt 1
Bill has a new highscore of 35
Sally has a new highscore of 40
John is on attempt 1
Fred is on attempt 1
.....
Sally is on attempt 8
Bill has a new highscore of 44
Bill is on attempt 9
Bill has a new highscore of 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)}}]