diff options
author | Stuart Sierra <mail@stuartsierra.com> | 2009-01-30 02:28:11 +0000 |
---|---|---|
committer | Stuart Sierra <mail@stuartsierra.com> | 2009-01-30 02:28:11 +0000 |
commit | 4ced903ec52317fc89256646cebac6508a4632ac (patch) | |
tree | 61273115d8d899f10abadae4eb390dc48411128a /src/clojure | |
parent | 7d64565250f47339b957728a480e36db9655765a (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.clj | 141 |
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)))) |