1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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))))
|