diff options
Diffstat (limited to 'src/clojure/contrib')
-rw-r--r-- | src/clojure/contrib/template.clj | 95 |
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)))) |