開始接觸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 ... #<user$add user$add@d3583e> [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)
最后小圖一張:
日本的偵探,名氣最大的當然是金田一 -無論爺爺還是孫子. by 青山剛昌
