我遇到了Clojure宏的一些限制。我想知道如何优化以下代码?
(defmacro ssplit-7-inefficient [x]
(let [t 7]
;; Duplicated computation here!
`(do [(first (split-with #(not (= '~t %)) '~x))
(drop 1 (second (split-with #(not (= '~t %)) '~x)))])))
(ssplit-7-inefficient (foo 7 bar baz))
;; Returns: [(foo) (bar baz)]
下面是一些行不通的方法:
(defmacro ssplit-7-fails [x]
(let [t 7]
`(do ((fn [[a b]] [a (drop 1 b)]) (split-with #(not (= '~t %)) '~x)))))
(ssplit-7-fails (foo 7 bar baz))
;; Error: Call to clojure.core/fn did not conform to spec.
(defmacro ssplit-7-fails-again [x]
(let [t 7]
`(do
(let [data (split-with #(not (= '~t %)) '~x)]
((fn [[a b]] [a (drop 1 b)]) data)))))
(ssplit-7-fails-again (foo 7 bar baz))
;; Error: Call to clojure.core/let did not conform to spec.
注意split-with
只分裂一次。你可以使用一些解构来得到你想要的:
(defmacro split-by-7 [arg]
`((fn [[x# [_# & z#]]] [x# z#]) (split-with (complement #{7}) '~arg)))
(split-by-7 (foo 7 bar baz))
=> [(foo) (bar baz)]
在其他用例中,partition-by
也很有用:
(defmacro split-by-7 [arg]
`(->> (partition-by #{7} '~arg)
(remove #{[7]})))
(split-by-7 (foo 7 bar baz))
=> ((foo) (bar baz))
在Clojure中对宏进行推理并不容易-(在我看来,macroexpand-1
与Common Lisp的macroexpand-1
相比,使代码疏远了很多…)
我的方法是先建立一个辅助函数。
(defn %split-7 [x]
(let [y 7]
(let [[a b] (split-with #(not= y %) x)]
[a (drop 1 b)])))
这个函数使用解构,使得split-with
是"有效的"。
它几乎完全完成了宏应该做的事情。只需要引用一下参数-使其有效。
(%split-7 '(a 7 b c))
;;=> [(a) (b c)]
从这一步到宏并不难。
宏应该在插入辅助函数调用时自动引用参数。
(defmacro split-7 [x]
`(%split-7 '~x))
所以我们可以调用:
(split-7 (a 7 b c))
;; => [(a) (b c)]
使用此技巧,甚至可以将函数泛化为:
(defn %split-by [x y]able like this
(let [[a b] (split-with #(not= y %) x)]
[a (drop 1 b)]))
(defmacro split-by [x y]
`(%split-by '~x ~y))
(split-by (a 7 b c) 7)
;; => [(a) (b c)]
(split-by (a 7 b c 9 d e) 9)
;; => [(a 7 b c) (d e)]
在宏体(甚至其他宏)或递归函数或递归宏(调用其他宏的宏)中使用(helper)函数显示了lisp宏是多么强大。因为它表明在制定/定义宏时可以使用整个lisp。这是大多数语言的宏通常做不到的。