aboutsummaryrefslogtreecommitdiff
path: root/src/clojure/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'src/clojure/contrib')
-rw-r--r--src/clojure/contrib/template.clj95
1 files changed, 95 insertions, 0 deletions
diff --git a/src/clojure/contrib/template.clj b/src/clojure/contrib/template.clj
new file mode 100644
index 00000000..d62cf69c
--- /dev/null
+++ b/src/clojure/contrib/template.clj
@@ -0,0 +1,95 @@
+;;; template.clj - anonymous functions that pre-evaluate sub-expressions
+
+;; By Stuart Sierra, http://stuartsierra.com/
+;; December 15, 2008
+
+;; Copyright (c) Stuart Sierra, 2008. 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 file defines the "template" macro. "template" is similar in
+;; spirit to #(). It has body expressions with "holes" represented by
+;; the symbols _1, _2, _3, and so on. ("_" is a synonym for "_1".)
+;; The holes become arguments to an anonymous function whose body is
+;; the template body. Unlike #() or "fn", however, any expressions
+;; that do not have any holes will be evaluated only once, at the time
+;; the function is created, not every time the function is called.
+;;
+;; Examples:
+;;
+(comment
+ ;; Assume we have some big, slow calculation.
+ (defn think-hard []
+ (Thread/sleep 1000)
+ 1000)
+
+ ;; With fn, think-hard gets called every time.
+ (time (doall (map (fn [x] (+ x (think-hard)))
+ (range 5))))
+ ;;=> "Elapsed time: 5001.33455 msecs"
+ ;;=> (1000 1001 1002 1003 1004)
+
+ ;; With a template, think-hard only gets called once.
+ (time (doall (map (template (+ _ (think-hard)))
+ (range 5))))
+ ;;=> "Elapsed time: 1000.907326 msecs"
+ ;;=> (1000 1001 1002 1003 1004)
+)
+;;
+;; You can use this to write macros that take a template as an
+;; argument.
+
+
+
+(ns clojure.contrib.template
+ (:use clojure.contrib.walk))
+
+(defn find-symbols
+ "Recursively finds all symbols in form."
+ [form]
+ (distinct (filter symbol? (tree-seq coll? seq form))))
+
+(defn find-holes
+ "Recursively finds all symbols starting with _ in form."
+ [form]
+ (sort (distinct (filter #(.startsWith (name %) "_")
+ (find-symbols form)))))
+
+(defn find-pure-exprs
+ "Recursively finds all sub-expressions in form that do not contain
+ any symbols starting with _"
+ [form]
+ (filter #(and (list? %)
+ (empty? (find-holes %)))
+ (tree-seq seq? seq form)))
+
+(defn flatten-map
+ "Transforms a map into a vector like [key value key value]."
+ [m]
+ (reduce (fn [coll [k v]] (conj coll k v))
+ [] m))
+
+(defmacro template
+ "Expands to a fn using _1, _2, _3, etc. as arguments (_ is the same
+ as _1). Any sub-expressions without any _* variables are evaluated
+ when the fn is created, not when it is called."
+ [& form]
+ (let [form (postwalk-replace {'_ '_1} form)
+ holes (find-holes form)
+ pures (find-pure-exprs form)
+ smap (zipmap pures (repeatedly #(gensym "HOLE_")))
+ newform (prewalk-replace smap form)
+ ;; Now, make sure we omit nested sub-expressions:
+ used (set (filter #(.startsWith (name %) "HOLE_")
+ (find-symbols newform)))
+ newmap (reduce (fn [m [k v]] (if (used v) (assoc m k v) m))
+ {} smap)]
+ `(let ~(flatten-map (clojure.set/map-invert newmap))
+ (fn ~(vec holes)
+ ~@newform))))