aboutsummaryrefslogtreecommitdiff
path: root/src/clojure
diff options
context:
space:
mode:
authorStuart Sierra <mail@stuartsierra.com>2009-01-30 02:28:11 +0000
committerStuart Sierra <mail@stuartsierra.com>2009-01-30 02:28:11 +0000
commit4ced903ec52317fc89256646cebac6508a4632ac (patch)
tree61273115d8d899f10abadae4eb390dc48411128a /src/clojure
parent7d64565250f47339b957728a480e36db9655765a (diff)
auto_agent.clj: new lib, auto-updating agents, like Cells in CL
Diffstat (limited to 'src/clojure')
-rw-r--r--src/clojure/contrib/auto_agent.clj141
1 files changed, 141 insertions, 0 deletions
diff --git a/src/clojure/contrib/auto_agent.clj b/src/clojure/contrib/auto_agent.clj
new file mode 100644
index 00000000..1d9275a7
--- /dev/null
+++ b/src/clojure/contrib/auto_agent.clj
@@ -0,0 +1,141 @@
+;;; auto_agent.clj: agents that update automatically from a formula
+
+;; by Stuart Sierra, http://stuartsierra.com/
+;; January 29, 2009
+
+;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use
+;; and distribution terms for this software are covered by the Eclipse
+;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
+;; which can be found in the file epl-v10.html at the root of this
+;; distribution. By using this software in any fashion, you are
+;; agreeing to be bound by the terms of this license. You must not
+;; remove this notice, or any other, from this software.
+
+
+
+;; This is superficially similar to Ken Tilton's "Cells" library for
+;; Common Lisp. But Cells is single-threaded and synchronous. This
+;; version is built on Clojure agents, so it is multi-threaded and
+;; asynchronous.
+;;
+;; An auto-agent is an agent whose value is the result of an
+;; expression. That expression can include other mutable Clojure
+;; types -- agents, atoms, and refs -- dereferenced with "deref" or
+;; "@". Whenever one of those derefernced things changes, the
+;; auto-agent will be automatically updated to reflect the new value.
+
+
+
+(ns clojure.contrib.auto-agent
+ (:use [clojure.contrib.walk :only (macroexpand-all)]
+ [clojure.contrib.test-is :only (deftest- is)]))
+
+(defmacro #^{:private true} find-derefs
+ "Expands the expression and returns the set of all agents/atoms/refs
+ dereferenced within it."
+ [expr]
+ (let [expr (macroexpand-all expr)]
+ (if (coll? expr)
+ (set (map second
+ (filter #(and (list? %)
+ (symbol? (first %))
+ (= (resolve (first %)) #'clojure.core/deref))
+ (tree-seq coll? seq expr))))
+ #{})))
+
+(defmacro auto-agent
+ "Creates an agent whose value is the result of evaluating expr.
+ Whenever one of the agents/atoms/refs dererenced within expr
+ changes, expr is reevaluated and this agent's state is set to its
+ new value."
+ [expr]
+ `(let [value-fn# (fn [old# a#] ~expr)
+ cell# (agent (value-fn# nil nil))]
+ (doseq [d# (find-derefs ~expr)]
+ (add-watcher d# :send cell# value-fn#))
+ cell#))
+
+
+
+;;; TESTS
+
+;; Run these tests with
+;; (clojure.contrib.test-is/run-tests 'clojure.contrib.auto-agent)
+
+;; Bind clojure.contrib.test-is/*load-tests* to false to omit these
+;; tests from production code.
+
+
+(deftest- auto-agents-can-depend-on-agents
+ (let [c1 (agent 1)
+ c2 (auto-agent (inc @c1))]
+ (is (= 1 @c1))
+ (is (= 2 @c2))
+ (send c1 inc)
+ (await c1 c2)
+ (is (= 2 @c1))
+ (is (= 3 @c2))))
+
+(deftest- auto-agents-can-depend-on-refs
+ (let [c1 (ref 1)
+ c2 (auto-agent (inc @c1))]
+ (is (= 1 @c1))
+ (is (= 2 @c2))
+ (dosync (alter c1 inc))
+ (await c2)
+ (is (= 2 @c1))
+ (is (= 3 @c2))))
+
+(deftest- auto-agents-can-depend-on-atoms
+ (let [c1 (atom 1)
+ c2 (auto-agent (inc @c1))]
+ (is (= 1 @c1))
+ (is (= 2 @c2))
+ (swap! c1 inc)
+ (await c2)
+ (is (= 2 @c1))
+ (is (= 3 @c2))))
+
+(deftest- very-long-dependency-chains-work-with-agents
+ (let [head (agent 1)
+ chain (take 10000 (iterate (fn [p] (auto-agent (inc @p))) head))
+ tail (auto-agent (inc @(last chain)))]
+ (is (= 1 @head))
+ (is (= 10001 @tail))
+ (send head inc)
+ ;; We can't just do (apply await head tail chain) because not all
+ ;; the actions have been sent by the time we call await. Instead,
+ ;; we have to await each cell in order. In general, you should
+ ;; not rely on agent cell updates happening at any specific time.
+ ;; This test is only to demonstrate that very long chains of agent
+ ;; cells can be made without causing a stack overflow, which is
+ ;; not true of ref/atom cells.
+ (await head)
+ (doseq [c chain] (await c))
+ (await tail)
+ (is (= 2 @head))
+ (is (= 3 @(second chain)))
+ (is (= 10001 @(last chain)))
+ (is (= 10002 @tail))))
+
+(deftest- agent-expressions-can-be-vectors
+ (let [c1 (agent 1)
+ c2 (agent 2)
+ c3 (auto-agent [@c1 @c2])]
+ (is (= [1 2] @c3))
+ (send c1 inc)
+ (send c2 inc)
+ (await c1 c2)
+ (await c3)
+ (is (= [2 3] @c3))))
+
+(deftest- agent-expressions-can-be-maps
+ (let [c1 (agent 1)
+ c2 (agent 2)
+ c3 (auto-agent {:c1 @c1, :c2 @c2})]
+ (is (= {:c1 1, :c2 2} @c3))
+ (send c1 inc)
+ (send c2 inc)
+ (await c1 c2)
+ (await c3)
+ (is (= {:c1 2, :c2 3} @c3))))