# [X分鐘速成Y](http://learnxinyminutes.com/)
## 其中 Y=clojure macros
源代碼下載:?[learnclojuremacros-zh.clj](http://learnxinyminutes.com/docs/files/learnclojuremacros-zh.clj)
和所有Lisp一樣,Clojure內在的[同構性](https://en.wikipedia.org/wiki/Homoiconic)使得你可以窮盡語言的特性,編寫生成代碼的子過程——“宏”。宏是一種按需調制語言的強大方式。
小心!可以用函數完成的事用宏去實現可不是什么好事。你應該僅在需要控制參數是否或者何時eval的時候使用宏。
你應該熟悉Clojure.確保你了解[Y分鐘學Clojure](http://learnxinyminutes.com/docs/zh-cn/clojure-cn/)中的所有內容。
~~~
;; 使用defmacro定義宏。宏應該輸出一個可以作為clojure代碼演算的列表。
;;
;; 以下宏的效果和直接寫(reverse "Hello World")一致。
(defmacro my-first-macro []
(list reverse "Hello World"))
;; 使用macroexpand或macroexpand-1查看宏的結果。
;;
;; 注意,調用需要引用。
(macroexpand '(my-first-macro))
;; -> (#<core$reverse clojure.core$reverse@xxxxxxxx> "Hello World")
;; 你可以直接eval macroexpand的結果
(eval (macroexpand '(my-first-macro)))
; -> (\d \l \o \r \W \space \o \l \l \e \H)
;; 不過一般使用以下形式,更簡短,更像函數:
(my-first-macro) ; -> (\d \l \o \r \W \space \o \l \l \e \H)
;; 創建宏的時候可以使用更簡短的引用形式來創建列表
(defmacro my-first-quoted-macro []
'(reverse "Hello World"))
(macroexpand '(my-first-quoted-macro))
;; -> (reverse "Hello World")
;; 注意reverse不再是一個函數對象,而是一個符號。
;; 宏可以傳入參數。
(defmacro inc2 [arg]
(list + 2 arg))
(inc2 2) ; -> 4
;; 不過,如果你嘗試配合使用引用列表,會導致錯誤,
;; 因為參數也會被引用。
;; 為了避免這個問題,clojure提供了引用宏的另一種方式:`
;; 在`之內,你可以使用~獲得外圈作用域的變量。
(defmacro inc2-quoted [arg]
`(+ 2 ~arg))
(inc2-quoted 2)
;; 你可以使用通常的析構參數。用~@展開列表中的變量。
(defmacro unless [arg & body]
`(if (not ~arg)
(do ~@body))) ; 別忘了 do!
(macroexpand '(unless true (reverse "Hello World")))
;; ->
;; (if (clojure.core/not true) (do (reverse "Hello World")))
;; 當第一個參數為假時,(unless)會演算、返回主體。
;; 否則返回nil。
(unless true "Hello") ; -> nil
(unless false "Hello") ; -> "Hello"
;; 需要小心,宏會搞亂你的變量
(defmacro define-x []
'(do
(def x 2)
(list x)))
(def x 4)
(define-x) ; -> (2)
(list x) ; -> (2)
;; 使用gensym來獲得獨有的標識符
(gensym 'x) ; -> x1281 (or some such thing)
(defmacro define-x-safely []
(let [sym (gensym 'x)]
`(do
(def ~sym 2)
(list ~sym))))
(def x 4)
(define-x-safely) ; -> (2)
(list x) ; -> (4)
;; 你可以在 ` 中使用 # 為每個符號自動生成gensym
(defmacro define-x-hygenically []
`(do
(def x# 2)
(list x#)))
(def x 4)
(define-x-hygenically) ; -> (2)
(list x) ; -> (4)
;; 通常會配合宏使用幫助函數。
;; 讓我們創建一些幫助函數來支持(無聊的)算術語法:
(declare inline-2-helper)
(defn clean-arg [arg]
(if (seq? arg)
(inline-2-helper arg)
arg))
(defn apply-arg
"Given args [x (+ y)], return (+ x y)"
[val [op arg]]
(list op val (clean-arg arg)))
(defn inline-2-helper
[[arg1 & ops-and-args]]
(let [ops (partition 2 ops-and-args)]
(reduce apply-arg (clean-arg arg1) ops)))
;; 在創建宏前,我們可以先測試
(inline-2-helper '(a + (b - 2) - (c * 5))) ; -> (- (+ a (- b 2)) (* c 5))
; 然而,如果我們希望它在編譯期執行,就需要創建宏
(defmacro inline-2 [form]
(inline-2-helper form)))
(macroexpand '(inline-2 (1 + (3 / 2) - (1 / 2) + 1)))
; -> (+ (- (+ 1 (/ 3 2)) (/ 1 2)) 1)
(inline-2 (1 + (3 / 2) - (1 / 2) + 1))
; -> 3 (事實上,結果是3N, 因為數字被轉化為帶/的有理分數)
~~~
## 擴展閱讀
[Clojure for the Brave and True](http://www.braveclojure.com/)系列的編寫宏 http://www.braveclojure.com/writing-macros/
官方文檔 http://clojure.org/macros
何時使用宏? http://dunsmor.com/lisp/onlisp/onlisp_12.html