aboutsummaryrefslogtreecommitdiff
path: root/src/clojure/contrib/template.clj
blob: 47b51953f1b01ed38ca325dc531a6b322e88ba77 (plain)
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
;;; 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 macros for using template expressions.  These are
;; useful for writing macros.
;;
;; A template is an expression containing "holes" represented by the
;; symbols _1, _2, _3, and so on.  ("_" is a synonym for "_1".)
;;
;; The "template" macro is similar to #().  It returns an anonymous
;; function containing the body of the template.  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 #(), think-hard gets called every time.
  (time (doall (map #(+ % (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)
)
;;
;;
;; There is also the do-template macro, which works differently.  It
;; calls the same template multiple times, filling in values, and puts
;; it all inside a "do" block.  It will split up the values based on
;; the number of holes in the template.
(comment
  (do-template (foo _1 _2) :a :b :c :d)
  ;; expands to: (do (foo :a :b) (foo :c :d))

  (do-template (foo _1 _2 _3) 10 11 12 13 14 15)
  ;; expands to: (foo 10 11 12) (foo 13 14 15)
  )



(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 ~(flatten-map (clojure.set/map-invert newmap))
     (fn ~(vec holes)
         ~@newform)))

(defn apply-template
  "Replaces _1, _2, _3, etc. in expr with corresponding elements of
  values.  Returns the modified expression.  For use in macros."
  [expr values]
  (let [expr (postwalk-replace {'_ '_1} expr)
        holes (find-holes expr)
        smap (zipmap holes values)]
    (prewalk-replace smap expr)))

(defmacro do-template
  "Repeatedly evaluates template expr (in a do block) using values in
  args.  args are grouped by the number of holes in the template.
  Example: (do-template (check _1 _2) :a :b :c :d)
  expands to (do (check :a :b) (check :c :d))"
  [expr & args]
  (let [expr (postwalk-replace {'_ '_1} expr)
        argcount (count (find-holes expr))]
    `(do ~@(map (fn [a] (apply-template expr a))
                (partition argcount args)))))