Skip to content

Commit

Permalink
Merge branch 'master' of git@github.com:onyin/lazy-agent
Browse files Browse the repository at this point in the history
  • Loading branch information
apatil committed Mar 25, 2009
2 parents 070aa2d + 7972753 commit a08d7dc
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 61 deletions.
6 changes: 5 additions & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ February 25, 2009
Creative Commons BY-SA, see LICENSE
copyright 2009 Anand Patil


Implements two types of agent-based 'cells' for Clojure: lazy agents and oblivious agents. These complement the auto-agents available in Clojure Contrib. Both allow for concurrent cell updates with respectably efficient scheduling and avoid unnecessarily repeating cell updates.

If you deref a lazy cell, you'll see a map: {:value xxx :status yyy}. :status may be :needs-update, :updating, :up-to-date or :oblivious. If a cell is up-to-date or oblivious, :value gives the value of the cell.

When a lazy agent's ancestor changes, its value changes to {:value nil :status :needs-update} but it does not compute its new value until it receives a message instructing it to do so. To send the update message to a group of agents, do (update a b c d e). To send the update message and wait for the values, do (evaluate a b c d e).

Oblivious agents are even lazier than lazy agents. When an oblivious agent is up-to-date, its status is :oblivious. If an ancestor subsequently changes, the oblivious agent will not do anything. It needs to receive a 'force-need-update' message to change state to {:value nil :status :needs-update}. After that, it behaves like a lazy agent until the next time it updates its value, at which point its status is reset to :oblivious.
Oblivious agents are even lazier than lazy agents. When an oblivious agent is up-to-date, its status is :oblivious. If an ancestor subsequently changes, the oblivious agent will not do anything. It needs to receive a 'force-need-update' message to change state to {:value nil :status :needs-update}. After that, it behaves like a lazy agent until the next time it updates its value, at which point its status is reset to :oblivious.


Lazy agents are guaranteed to update only once per 'update' call. They will not update until all of their parents are up-to-date.
115 changes: 56 additions & 59 deletions lazy-agent.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@
; Good reference on scheduling: Scheduling and Automatic Parallelization.
; Chapter 1 covers scheduling in DAGs and is available free on Google Books.

; TODO: Make a fn analogous to synchronize that adds a watcher with a specified action to cells, which waits till they compute and then dispatches the action with the cells' values.
; TODO: Abbreviations:
;- la : lazy agent
;- p : parent
;- pv : parent value / parent val
;- cv : cell value / cell val
;- cm : cell meta
;- obliv : oblivious
; TODO: Shorten code with macros.
; TODO: Make a fn analogous to synchronize that adds a watcher with a specified action to cells, which waits till they compute and then dispatches the action with the cells' vals.
; TODO: Propagate exceptions.
; TODO: Transactions won't actually work for rejecting jumps. When a cell computes, have its update-fn memoize its last arguments.
; TODO: Transactions won't actually work for pipelining either. Just make this one actor-based, then make an implementation later that's truly stateless.
Expand All @@ -23,49 +15,54 @@

;(set! *warn-on-reflection* true)


; ==================================================
; = Utility stuff not immediately related to cells =
; ==================================================
(ns lazy-agent)

(defmacro structmap-and-accessors [sym & fields]
"Defunes a structmap with given symbol, and automatically creates accessors for all its fields."
(let [code-lst `(defstruct ~sym ~@fields)
sym-dash (.concat (name sym) "-")
accessor-names (zipmap fields (map (comp #(.concat sym-dash %1) name) fields))]
(cons 'do (cons code-lst
(for [field fields] (let [n (accessor-names field) s (symbol n)]
`(def ~s (accessor ~sym ~field))))))))

(defn agent? [x] (instance? clojure.lang.Agent x))
(defn id? [x] (instance? clojure.lang.IDeref x))
(defn deref-or-val [x] (if (id? x) @x x))
(defn map-now [fn coll] (dorun (map fn coll)))

; ============================================
; = Structmaps and accessors for cell values =
; = Structmaps and accessors for cell vals =
; ============================================
(defstruct cell-val :value :status)
(def cell-value (accessor cell-val :value))
(def cell-status (accessor cell-val :status))
(def needs-update-value (struct cell-val nil :needs-update))
(def deref-cell (comp cell-value deref))
(structmap-and-accessors cell-val :val :status)
(def needs-update-val (struct cell-val nil :needs-update))
(def deref-cell (comp cell-val-val deref))

(defstruct cell-meta :agent-parents :id-parent-vals :n-id-parents :parents :fn :oblivious? :lazy-agent)
(def cell-meta-agent-parents (accessor cell-meta :agent-parents))
(def cell-meta-id-parent-vals (accessor cell-meta :id-parent-vals))
(def cell-meta-n-id-parents (accessor cell-meta :n-id-parents))
(def cell-meta-parents (accessor cell-meta :parents))
(def cell-meta-fn (accessor cell-meta :fn))
(def cell-meta-oblivious? (accessor cell-meta :oblivious?))
(def cell-meta-lazy-agent (accessor cell-meta :lazy-agent))
(structmap-and-accessors cell-meta :agent-parents :id-parent-vals :n-id-parents :parents :fn :oblivious? :lazy-agent)
(defn is-lazy-agent? [x] (-> x deref meta :lazy-agent))

(defn up-to-date? [cell] (= :up-to-date (cell-status cell)))
(defn oblivious? [cell] (= :oblivious (cell-status cell)))
(defn updating? [cell] (= :updating (cell-status cell)))
(defn needs-update? [cell] (= :needs-update (cell-status cell)))
(defn up-to-date? [cell] (= :up-to-date (cell-val-status cell)))
(defn oblivious? [cell] (= :oblivious (cell-val-status cell)))
(defn inherently-oblivious? [cell-val] (-> cell-val meta cell-meta-oblivious?))
(defn updating? [cell] (= :updating (cell-val-status cell)))
(defn needs-update? [cell] (= :needs-update (cell-val-status cell)))
(defn second-arg [x y] y)
(defn set-agent! [a v] (send a second-arg v))
(defn set-cell! [c v] (send c (fn [x] (struct cell-val :value v :status :up-to-date))))

(defn set-cell! [c v]
"Sets a cell's val to v, and sets its status to either :updated or :oblivious as appropriate."
(send c
(fn [old-v] (let [updated-status (if (inherently-oblivious? old-v) :oblivious :up-to-date)]
(with-meta (struct cell-val v updated-status) (meta v))))))

; ==================
; = Updating stuff =
; ==================

(defn updating-fn [x] (if (needs-update? x) (assoc x :status :updating) x))
(defn force-need-update-fn [x] (with-meta needs-update-value (meta x)))
(defn force-need-update-fn [x] (with-meta needs-update-val (meta x)))
(defn send-force-need-update [p] "Utility function that puts p into the needs-update state, even if p is oblivious." (send p force-need-update-fn))
(defn send-update [p] "Utility function that puts p into the updating state if it needs an update." (send p updating-fn))

Expand All @@ -74,51 +71,51 @@

(defn complete-parents [parent-val-map parents]
"Takes a map of the form {parent @parent}, and a list of mutable and
immutable parents, and returns a list of the parents' values in the
immutable parents, and returns a list of the parents' vals in the
correct order."
(loop [parents-sofar parents val-sofar (list)]
(if (empty? parents-sofar) val-sofar
(let [parent (last parents-sofar)
rest-parents (butlast parents-sofar)]
(if (id? parent)
; If value has a key corresponding to this parent, cons the corresponding value
; If val has a key corresponding to this parent, cons the corresponding val
(recur rest-parents (cons (parent-val-map parent) val-sofar))
; Otherwise, cons the parent.
(recur rest-parents (cons parent val-sofar)))))))

(defn compute-cell-value [cur-val cur-meta id-parent-vals new-status]
"Can be sent to a cell when its id-parent-vals are complete to compute its value."
(defn compute-cell-val [cur-val cur-meta id-parent-vals new-status]
"Can be sent to a cell when its id-parent-vals are complete to compute its val."
(let [parents (cell-meta-parents cur-meta)
update-fn (cell-meta-fn cur-meta)
new-parents (complete-parents id-parent-vals parents)
new-val (apply update-fn new-parents)]
; Create new value, preserving metadata, and put cell in either up-to-date or oblivious state.
; Create new val, preserving metadata, and put cell in either up-to-date or oblivious state.
(with-meta (struct cell-val new-val new-status) cur-meta)))

(defn swap-la-parent-value [parent-val-map parent parent-val]
(defn swap-la-parent-val [parent-val-map parent parent-val]
"Utility function that incorporates updated parents into a cell's
parent value ref."
parent val ref."
; If the parent is a lazy agent, check whether it's switched into the
; needs update state, otherwise record its new value.
; needs update state, otherwise record its new val.
(if (needs-update? parent-val)
(dissoc parent-val-map parent)
(assoc parent-val-map parent (cell-value parent-val))))
(assoc parent-val-map parent (cell-val-val parent-val))))

(defn swap-id-parent-value [parent-val-map parent parent-val]
; Otherwise, just record its new value.
(defn swap-id-parent-val [parent-val-map parent parent-val]
; Otherwise, just record its new val.
(assoc parent-val-map parent parent-val))

(defn reaction [val meta]
; If the child is not oblivious, put it in the needs-update state.
(with-meta (if (needs-update? val) val needs-update-value) meta))
(with-meta (if (needs-update? val) val needs-update-val) meta))

(defn report-to-child [parent-lazy-agent? oblivious?]
"Called by parent-watcher when a parent either updates or reverts to
the 'needs-update' state. If a parent updates and the child cell wants
to update, computation is performed if possible. If a parent reverts
to the needs-update state, the child is put into the needs-update
state also."
(let [swap-fn (if parent-lazy-agent? swap-la-parent-value swap-id-parent-value)
(let [swap-fn (if parent-lazy-agent? swap-la-parent-val swap-id-parent-val)
react-fn (if oblivious? with-meta reaction)
updated-status (if oblivious? :oblivious :up-to-date)]
(fn [cur-val parent parent-val]
Expand All @@ -129,9 +126,9 @@
(if (updating? cur-val)
(if (= (count new-id-parent-vals) (cell-meta-n-id-parents new-meta))
; Compute if possible, otherwise do nothing.
(compute-cell-value cur-val new-meta new-id-parent-vals updated-status)
(compute-cell-val cur-val new-meta new-id-parent-vals updated-status)
(with-meta cur-val new-meta))
; React to the new value.
; React to the new val.
(react-fn cur-val new-meta))))))

(defn watcher-to-watch [fun]
Expand All @@ -143,7 +140,7 @@

(defn parent-watcher [oblivious?]
"Watches a parent cell on behalf of one of its children. This watcher
has access to a ref which holds the values of all the target child's
has access to a ref which holds the vals of all the target child's
updated parents. It also reports parent chages to the child."
(let [report (report-to-child true oblivious?)]
(fn [cur-val p p-val]
Expand All @@ -164,7 +161,7 @@
; If the cell has changed into the updating state, check whether an immediate computation is possible.
(if (= num-id-parent-vals num-id-parents)
; Compute if possible
(send cell compute-cell-value cell-meta id-parent-vals updated-status)
(send cell compute-cell-val cell-meta id-parent-vals updated-status)
; Otherwise put all parents that need updates into the updating state.
(map-now send-update (cell-meta-agent-parents cell-meta)))))))

Expand All @@ -173,18 +170,18 @@
; =======================

(defn updated? [c] (not (= (-> c deref :status) :needs-update)))
(defn extract-value [x] (let [v (:value x)] (if v v x)))
(def extract-cell-value (comp extract-value deref))
(defn extract-val [x] (let [v (:val x)] (if v v x)))
(def extract-cell-val (comp extract-val deref))
(defn cell [name update-fn parents & [oblivious?]]
"Creates a cell (lazy auto-agent) with given update-fn and parents."
(let [parents (vec parents)
id-parents (set (filter id? parents))
n-id-parents (count id-parents)
agent-parents (set (filter agent? id-parents))
updated-parents (filter updated? id-parents)
id-parent-vals (zipmap updated-parents (map extract-cell-value updated-parents))
id-parent-vals (zipmap updated-parents (map extract-cell-val updated-parents))
cell (agent (with-meta
needs-update-value
needs-update-val
(struct cell-meta agent-parents id-parent-vals n-id-parents parents update-fn oblivious? true)))
add-parent-watcher (fn [p] (add-watch p cell (watcher-to-watch
(if (is-lazy-agent? p) (parent-watcher oblivious?) (report-to-child false oblivious?)))))]
Expand All @@ -199,14 +196,14 @@
"Creates and inters a cell in the current namespace, bound to sym,
with given parents and update function."
[sym update-fn parents & [oblivious?]]
`(def ~sym (cell ~@(name sym) ~update-fn ~parents ~oblivious?)))
`(def ~sym (cell ~(name sym) ~update-fn ~parents ~oblivious?)))

; =======================================
; = Synchronized multi-cell evaluations =
; =======================================
(defn not-waiting? [cell-val]
"Determines whether a cell is either up-to-date or oblivious."
(let [status (cell-status cell-val)]
(let [status (cell-val-status cell-val)]
(or
(= :up-to-date status)
(= :oblivious status))))
Expand All @@ -221,7 +218,7 @@

(def cell-waiting? (comp not not-waiting? deref))
(defn evaluate [& cells]
"Updates the cells, waits for them to compute, and returns their values."
"Updates the cells, waits for them to compute, and returns their vals."
(let [
latch (java.util.concurrent.CountDownLatch. (count (filter cell-waiting? cells)))
watcher-adder (fn [cell] (add-watch cell latch unlatching-watcher))
Expand All @@ -241,7 +238,7 @@
(apply update cells)))

(defn force-evaluate [& cells]
"Forces the cells to update, waits for them and returns their values."
"Forces the cells to update, waits for them and returns their vals."
(do
(apply force-need-update cells)
(apply await cells)
Expand All @@ -256,7 +253,7 @@
(let [dissoc-map (if dissoc-cond (dissoc old-map old-key) old-map)
val (deref-or-val new-key)]
(if assoc-cond
(if (up-to-date? val) (assoc dissoc-map new-key (extract-value val)) dissoc-map)
(if (up-to-date? val) (assoc dissoc-map new-key (extract-val val)) dissoc-map)
dissoc-map)))

(defn conditional-set-replace [old-set old-val new-val disj-cond conj-cond]
Expand All @@ -280,15 +277,15 @@
agent-parents (conditional-set-replace (cell-meta-agent-parents old-meta) old-parent new-parent old-agent? new-agent?)
id-parent-vals (conditional-map-replace (cell-meta-id-parent-vals old-meta) old-parent new-parent old-id? new-id?)
n-id-parents (conditional-counter-change (cell-meta-n-id-parents old-meta) old-id? new-id?)]
(with-meta (if (= (cell-status cell-val) :oblivious) cell-val needs-update-value)
(with-meta (if (= (cell-val-status cell-val) :oblivious) cell-val needs-update-val)
(assoc old-meta
:parents parents
:agent-parents agent-parents
:id-parent-vals id-parent-vals
:n-id-parents n-id-parents))))

(defn replace-parent [cell old-parent new-parent]
"Replaces a cell's parent. Sets the cell's value to needs-update, or leaves
"Replaces a cell's parent. Sets the cell's val to needs-update, or leaves
it unchanged if the cell is oblivious."
(do
; Remove old watcher
Expand Down
5 changes: 4 additions & 1 deletion test.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(load-file "lazy-agent.clj")
(refer 'lazy-agent :only ['def-cell 'update 'evaluate 'set-agent! 'force-need-update 'force-evaluate 'force-update 'replace-parent])
(refer 'lazy-agent :only ['def-cell 'update 'evaluate 'force-need-update 'force-evaluate 'force-update 'replace-parent])

(defn set-agent! [a v] (send a (fn [x] v)))

(defn sleeping [fun]
(fn [& x] (do (Thread/sleep 100) (apply fun x))))
Expand All @@ -13,6 +15,7 @@
(def-cell e (sleeping +) [a 2] true)
(def-cell f (sleeping +) [c e 12])


(time (evaluate d e f))
(set-agent! x 13)
(time (evaluate d e f))
Expand Down

0 comments on commit a08d7dc

Please sign in to comment.