2011-05-26 43 views
2

这是我的第一个Clojure宏 - 我是一个超级菜鸟。用于字符串模板替换的Clojure宏

昨天我posted并精炼了一个字符串模板替换函数。有几个人建议在编译时可以更换密钥。这是我第一次尝试:

(defn replace-templates* 
    "Return a String with each occurrence of a substring of the form {key} 
    replaced with the corresponding value from a map parameter. 
    @param str the String in which to do the replacements 
    @param m a map of template->value 
    @thanks kotarak https://stackoverflow.com/questions/6112534/ 
    follow-up-to-simple-string-template-replacement-in-scala-and-clojure" 
    [^String text m] 
    (let [builder (StringBuilder.)] 
    (loop [text text] 
     (cond 
     (zero? (count text)) 
     (.toString builder) 

     (.startsWith text "{") 
     (let [brace (.indexOf text "}")] 
      (if (neg? brace) 
      (.toString (.append builder text)) 
      (if-let [[_ replacement] (find m (subs text 1 brace))] 
       (do 
       (.append builder replacement) 
       (recur (subs text (inc brace)))) 
       (do 
       (.append builder "{") 
       (recur (subs text 1)))))) 

     :else 
     (let [brace (.indexOf text "{")] 
      (if (neg? brace) 
      (.toString (.append builder text)) 
      (do 
       (.append builder (subs text 0 brace)) 
       (recur (subs text brace))))))))) 

(def foo* 42) 
(def m {"foo" foo*}) 

(defmacro replace-templates 
    [text m] 
    (if (map? m) 
    `(str 
     [email protected](loop [text text acc []] 
     (cond 
      (zero? (count text)) 
      acc 

      (.startsWith text "{") 
      (let [brace (.indexOf text "}")] 
      (if (neg? brace) 
       (conj acc text) 
       (if-let [[_ replacement] (find m (subs text 1 brace))] 
       (recur (subs text (inc brace)) (conj acc replacement)) 
       (recur (subs text 1) (conj acc "{"))))) 

      :else 
      (let [brace (.indexOf text "{")] 
      (if (neg? brace) 
       (conj acc text) 
       (recur (subs text brace) (conj acc (subs text 0 brace)))))))) 
    `(replace-templates* ~text m))) 

(macroexpand '(replace-templates "this is a {foo} test" {"foo" foo*})) 
;=> (clojure.core/str "this is a " foo* " test") 
(println (replace-templates "this is a {foo} test" {"foo" foo*})) 
;=> this is a 42 test 
(macroexpand '(replace-templates "this is a {foo} test" m)) 
;=> (user/replace-templates* "this is a {foo} test" user/m) 
(println (replace-templates "this is a {foo} test" m)) 
;=> this is a 42 test 

有没有更好的方法来编写这个宏?特别是每个值的扩展版本都没有获得命名空间限定。

+0

用'(地图?m)'好主意。 :) – kotarak 2011-05-26 20:35:53

回答

1

我会尽量减少重复的东西。我调整了函数以使用累加器的宏观方法,并让replace-templates*通过(apply str ...)完成其余部分。这样就可以重新使用宏中的函数。

(defn extract-snippets 
    [^String text m] 
    (loop [text  text 
     snippets []] 
    (cond 
     (zero? (count text)) 
     snippets 

     (.startsWith text "{") 
     (let [brace (.indexOf text "}")] 
     (if (neg? brace) 
      (conj snippets text) 
      (if-let [[_ replacement] (find m (subs text 1 brace))] 
      (recur (subs text (inc brace)) (conj snippets replacement)) 
      (recur (subs text 1)   (conj snippets \{))))) 

     :else 
     (let [brace (.indexOf text "{")] 
      (if (neg? brace) 
      (conj snippets text) 
      (recur (subs text brace) (conj snippets (subs text 0 brace)))))))) 

(defn replace-templates* 
    [text m] 
    (apply str (extract-snippets text m))) 

(defmacro replace-templates 
    [text m] 
    (if (map? m) 
    `(apply str ~(extract-snippets text m)) 
    `(replace-templates* ~text ~m))) 

注意:在您的宏中,您没有取消引用m。所以它只能起作用,因为你之前已经确定了它。它不会与(let [m {"a" "b"}] (replace-templates "..." m))

+0

感谢您的重构。我正在阅读“Let Over Lambda”(Doug Hoyte) - 这都是关于Common Lisp宏的。还要感谢'〜m'上的答案。我早些时候尝试过,但由于我的宏中的其他问题,没有注意到这是“正确的”答案。 – Ralph 2011-05-26 20:48:22

0

更改(defn m {"foo" foo*})(def m {"foo" foo*})它似乎工作。

+0

是的,我已经找到了一个:-)。 – Ralph 2011-05-26 17:30:45