开始接触Lisp宏是看Ansi Common Lisp的第十章,Lisp宏定义相关的话题都已经提到,有兴趣的可以看看.ACL的目前已经在Github上有中文译本 [ 第十章],不要太担心Clojure与Lisp的语法差异,可以看下面这个对照表 http://clojure.org/lisps .
Clojure 宏给人留下第一印象就是各种符号
` ' ~ ~@ ,那就从这些符号怎么读开始吧
怎么读
user=> (defmacro foreach [[sym coll] & body] `(loop [coll# ~coll] (when-let [[~sym & xs#] (seq coll#)] ~@body (recur xs#)))) #'user/foreach user=> user=> (foreach [x [1 2 3]] (println x)) 1 2 3 nil user=>
先看看 Clojure 官方文档是怎么称呼这几个符号的:
Syntax-quote (`, note, the "backquote" character), Unquote (~) and Unquote-splicing (~@) For all forms other than Symbols, Lists, Vectors, Sets and Maps, `x is the same as 'x.
怎么用
知道了名字,下面就要看看符号的作用了:
Syntax-quote `防止宏内部的表达式求值,宏代码体内的代码替换到使用这个宏的地方.如果仅仅代码文本的替换,灵活性就有限了,我们使用
unquote符号~进行不宏代码体内的表达式;如果symbol代表的是一个seq,那么我们可以使用
Unquote-splicing (~@) 进行seq数据项的展开.
看例子:
user=> (defmacro dbg[x] `(let [x# ~x] (println '~x "=" x#) x#)) user=> (def x 5) user=> (def lst '(a b c)) user=> `(fred x ~x lst ~@lst 7 8 :nine) (user/fred user/x 5 user/lst a b c 7 8 :nine) user=> `(abc ~(symbol (str "i" "s" \- "cool"))) (user/abc is-cool) user=> `(max ~@(shuffle (range 10))) (clojure.core/max 8 7 1 9 0 6 4 2 3 5)
` '的区别
Clojure
' `区别在于
Syntax-quote (`)会进行symbol的解析
user=> '(foo bar)
(foo bar)
user=> `(foo bar)
(user/foo user/bar)
下面的代码中Syntax-quote 包含的代码中包含symbol x,而在当前的代码空间并没有user/x的定义,所以抛出了异常:
user=> (defmacro debug [x] `(println ">>" '~x ":" ~x x)) #'user/debug user=> (let [a 10] (debug a)) CompilerException java.lang.RuntimeException: No such var: user/x, compiling:(NO _SOURCE_PATH:72) user=>
我们暂时把x随便换成一个数字23让代码可以执行,可以看到其它部分的代码都是正确的:
user=> (defmacro debug [x] `(println ">>" '~x ":" ~x 23)) #'user/debug user=> (let [a 10] (debug a)) >> a : 10 23 nil user=>
syntax-quote 嵌套
syntax-quote 将symbol解析成为fully-qulified symbol,所谓fully-qulified symbol 就是形如namespace/name或fully.qualified.Classname 如果是symbol是非名称空间限定的(non-namespace-qualified)且以#符号结尾,会解析成为name_uniqueid的形式比如x_123.
这里遇到一个比较郁闷的问题就是syntax-quote嵌套,这个在Shell中测试的代码和直觉并不一致,我的问题是:
user=> `'y
(quote user/y)
这个是可以理解的,'y等价(quote y),`'y 也就是`(quote y),结果是(quote user/y)
但是``y 的结果和我预期的不一致:
user=> ``y
(quote user/y)
我想的是`y 的结果是user/y,然后`user/y的结果是user/y,也就是说``y的结果应该是user/y
请教了豆瓣的友邻 @huangz 得到解答:如果 syntex-quote 里面包含的是 resloved symbol ,那就简单的使用quote包围一下.这样对于上面的代码,``y结果是 (quote user/y),就可以理解了. 解答这种问题最好的方式就是看一下Reader的逻辑实现: https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LispReader.java
' 的实现 dispatchMacros['\''] = new VarReader(); public static class VarReader extends AFn{ public Object invoke(Object reader, Object quote) { PushbackReader r = (PushbackReader) reader; Object o = read(r, true, null, true); return RT.list(THE_VAR, o); } } `的实现 macros['`'] = new SyntaxQuoteReader(); public static class SyntaxQuoteReader extends AFn { // 代码省略........ } ~的实现 macros['~'] = new UnquoteReader(); static class UnquoteReader extends AFn{ public Object invoke(Object reader, Object comma) { PushbackReader r = (PushbackReader) reader; int ch = read1(r); if(ch == -1) throw Util.runtimeException("EOF while reading character"); if(ch == '@') { Object o = read(r, true, null, true); return RT.list(UNQUOTE_SPLICING, o); } else { unread(r, ch); Object o = read(r, true, null, true); return RT.list(UNQUOTE, o); } } }
通过下面这个几乎只会在试卷中出现的代码检查一下我们对符号嵌套的理解吧,平时没有人心理扭曲到写这种无聊的代码吧
user=> (let [x 9, y '(- x)] (println 0 y) (println 1 `y) (println 2 ``y) (println 3 ```y) ;(println 4 ~y) (println 5 `~y) (println 6 ``~y) (println 7 ``~~y)) 0 (- x) 1 user/y 2 (quote user/y) 3 (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y)))) 5 (- x) 6 user/y 7 (- x) nil user=>
其实还可以更无聊一点,定义 x y 我们后面的测试就围绕这两个变量展开:
user=> (def x 12) #'user/x user=> (def y 23) #'user/y user=> `y user/y user=> ``y (quote user/y) user=> `'y (quote user/y) user=> ```y (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y)))) user=> ````y (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/seq)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/concat)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/list)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote quote)))))))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/list)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y))))))))))))) 下面开始折腾~运算符,首先看到~需要在`的情况下才有效,否则就会有下面这种错误 user=> ~y IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote clojure.lang.Var$Unbound.throwArity (Var.java:43) user=> `~y 23 user=> `'y (quote user/y) user=> 'y y user=> `'~y (quote 23) user=> `23 23 user=> `~~y IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote clojure.lang.Var$Unbound.throwArity (Var.java:43) user=> ``~~y 23 user=> `~'y y user=> (= `y 'y) false user=> (= y 'y) false user=> 'y y user=> (= 'y (quote y)) true user=> (quote y) y user=> ``~~y 23 user=> ``~y user/y user=> `~y 23 user=> (macexpand-1 ``~~y) CompilerException java.lang.RuntimeException: Unable to resolve symbol: macexpand-1 in this context, compiling:(NO_SOURCE_PATH:133) user=> (macroexpand-1 ``~~y) 23 user=> `'23 (quote 23) user=>
宏展开
"Clojure Programming" 书中有一个Clojure编译的流程图:
我们可以使用macroexpand和macroexpand-1这样个辅助方法来查看宏展开的情况,下面是我们测试的代码:
user=> (defmacro ya-defn [fn-name args & body] `(defn ~fn-name ~args (println "Calling ..." ~fn-name ~args) ~@body)) #'user/ya-defn user=> (ya-defn add [a b] (+ a b)) #'user/add user=> (add 2 3) Calling ... #[ 2 3] 5
编译器进行宏展开,宏产出的代码成为原始程序的一部分.可以通过调用macroexpand-1 或者macroexpand 查看宏展开的结果,这两个函数的区别在于macroexpand会反复调用macroexpand-1进行宏展开,直到没有宏为止.下面的例子可以看到这个差别,注意为了清晰看到代码结构我在前面添加了pprint的函数调用.
user=> (macroexpand-1 '(ya-defn add [a b] (+ a b))) (clojure.core/defn add [a b] (clojure.core/println "Calling ..." add [a b]) (+ a b)) user=> (pprint (macroexpand-1 '(ya-defn add [a b] (+ a b)))) (clojure.core/defn add [a b] (clojure.core/println "Calling ..." add [a b]) (+ a b)) nil user=> (pprint (macroexpand '(ya-defn add [a b] (+ a b)))) (def add (clojure.core/fn ([a b] (clojure.core/println "Calling ..." add [a b]) (+ a b)))) nil
下面是函数macroexpand和macroexpand-1的源码实现,代码胜千言:
user=> (source macroexpand) (defn macroexpand "Repeatedly calls macroexpand-1 on form until it no longer represents a macro form, then returns it. Note neither macroexpand-1 nor macroexpand expand macros in subforms." {:added "1.0" :static true} [form] (let [ex (macroexpand-1 form)] (if (identical? ex form) form (macroexpand ex)))) nil user=> (source macroexpand-1) (defn macroexpand-1 "If form represents a macro form, returns its expansion, else returns form." {:added "1.0" :static true} [form] (. clojure.lang.Compiler (macroexpand1 form))) nil user=>
auto_gensym机制
我们想创建一个
unqualified symbol
的时候,就会在symbol的后面添加#符号.
user=> `(x#) (x__6__auto__) ;;;定义dbg user=> (defmacro dbg[x] `(let [x# ~x] (println '~x "=" x#) x#)) #'user/dbg user=> (defn pythag [x,y] (*(* x x) (* y y))) #'user/pythag user=> (pythag 5 6 ) 900 user=> (defn pythag [x,y] (dbg(* (dbg (* x x)) (dbg (* y y))))) #'user/pythag user=> (pythag 5 6 ) (* x x) = 25 (* y y) = 36 (* (dbg (* x x)) (dbg (* y y))) = 900 900
从代码中学习
学习Macro,自我感觉比较好的学习方式就是看Clojure中宏的实现,尝试自己写一下.再看两个宏的例子:通过source函数查看->和 ->>的内部实现:
user=> ( -> 25 Math/sqrt int list) (5) user=> ( ->> 25 Math/sqrt int list) (5) user=> (source ->) (defmacro -> "Threads the expr through the forms. Inserts x as the second item in the first form, making a list of it if it is not a list already. If there are more forms, inserts the first form as the second item in second form, etc." {:added "1.0"} ([x] x) ([x form] (if (seq? form) (with-meta `(~(first form) ~x ~@(next form)) (meta form)) (list form x))) ([x form & more] `(-> (-> ~x ~form) ~@more))) nil user=> (source ->>) (defmacro ->> "Threads the expr through the forms. Inserts x as the last item in the first form, making a list of it if it is not a list already. If there are more forms, inserts the first form as the last item in second form, etc." {:added "1.1"} ([x form] (if (seq? form) (with-meta `(~(first form) ~@(next form) ~x) (meta form)) (list form x))) ([x form & more] `(->> (->> ~x ~form) ~@more))) nil user=>
附 一段很实用的宏:
user=> (source2 kw)
(defmacro kw
"查询当前所有ns中含特定字符串的函数,如: (kw -index)"
[s] `(filter #(>= (.indexOf (str %) (name '~s)) 0)
(sort (keys (mapcat ns-publics (all-ns))))))
nil
user=> (kw source)
(*source-path* read-resource resource source source-fn source-fn2 source2)
user=> (kw -index)
(keep-indexed map-indexed safe-index)
(defmacro kw
"查询当前所有ns中含特定字符串的函数,如: (kw -index)"
[s] `(filter #(>= (.indexOf (str %) (name '~s)) 0)
(sort (keys (mapcat ns-publics (all-ns))))))
nil
user=> (kw source)
(*source-path* read-resource resource source source-fn source-fn2 source2)
user=> (kw -index)
(keep-indexed map-indexed safe-index)
来源: http://www.newsmth.net/nForum/#!article/FuncProgram/25982
[0] http://stackoverflow.com/questions/3704372/how-does-clojures-syntax-quote-work
[1] https://github.com/hiredman/clojure/blob/readerII/src/clj/clojure/reader.clj#L367
[2] http://blog.8thlight.com/colin-jones/2012/05/22/quoting-without-confusion.html
[3] http://clojure.org/reader
[4] http://clojure.org/lisps
[5] https://github.com/acl-translation/acl-chinese/blob/master/zhCN/ch10-cn.rst
[6] http://en.wikibooks.org/wiki/Learning_Clojure/Reader_Macros