宏(macro)是 Lisp 語言中最重要的武器,它能夠自動生成運行時的代碼。宏也是編寫領域特定語言(DSL)的利器,能夠在不改動語言自己的基礎上,增長新的程序構造體,這在其餘語言中是不可能。好比,如今比較流行的同步方式寫異步代碼的 async/await,在非 Lisp 語言須要語言自己支持,可是在 Lisp 裏面能夠經過幾個宏來解決,能夠參考:core.async。html
With great power comes great responsibility.java
因爲宏的強大,掌握編寫它的方法有較大的難度,因此社區通常會建議能不用就不要用它,我我的也比較認同這一點,可是對於一些場景用宏確實也很方便,能使程序簡潔明瞭,因此仍是有必要掌握它的,等有必定經驗,就能夠知道在什麼場景下使用最合適了。git
爲了由淺入深、系統地介紹宏,打算分兩篇文章來介紹,第一篇爲理論篇,主要介紹github
Symbol 數據類型,爲何須要它,以及其餘非 Lisp 語言爲何沒有express
宏的本質,宏運行時期編程
第二篇是實戰篇,介紹宏的一些常見技巧以及一些通用宏。這兩篇均以 Clojure 方言爲示例,但其概念原理在其餘 Lisp 中都是相通的。本文爲第一篇。ruby
在 Clojure 裏面,除了 number,string,list,map 等常見基本數據類型外,還有一個比較特殊的 Symbol 類型,定義以下:閉包
Symbols are identifiers that are normally used to refer to something else.異步
能夠說 symbol 就是一些標識符,用來代指其餘東西,和英文中 he/she/it 等「代詞」的做用差很少。
如今編程使用的都是高級語言,其語言構造體(language's constructs)是由一些關鍵字與用戶自定義變量組成,而這些都是 symbol,好比下面一個 Hello World 的示例:async
(defn hello [greeting] (println "Hello " greeting)) (hello "world")
上面的 defn, hello, greeting, println 都是 symbol,用來指向其餘數據類型,在進行求值(eval)時,Compiler 會計算出相應值。Clojure 裏面提供了 def 這個special form來創建 symbol 到其餘值的映射關係。例如:
user> (def cat "Tom") #'user/cat
簡單來講上面這一句把 symbol 執行了字符串Tom
,可是因爲 Clojure 裏面爲了實現其動態特性,真實的狀況稍微複雜一些,見下圖:
Var 是Clojure裏面提供的四種引用類型中最經常使用的,支持動態做用域以及 thread-local 值。動態做用域是指函數內的自由變量的值是在運行時肯定的,這裏不清楚的能夠參考個人另外一篇文章《編程語言中的變量做用域與閉包》,這裏不在贅述。
繼續回到上面的圖,def 會把 cat 這個 symbol 指向同名的 var,同時把 var 指向字符串 Tom(叫作 root binding)。symbol 到 var 的映射關係保存在每一個 namespace 中,能夠用resolve
來查詢這個映射關係:
user> (resolve 'cat) #'user/cat
最後一點須要注意的是,symbol 在 var 的映射關係只有在用 def 定義全局 var 是才具備,使用 let, loop 定義的詞法做用域(lexical context)裏面的 binding 不算是變量,一旦建立後沒法修改。可使用 with-local-vars
宏來定義局部變量:
(with-local-vars [x 1 y 2] (var-set x 11) (+ (var-get x) (var-get y))) ;; => 13
由上面介紹咱們能夠知道,Clojure 編譯器在遇到 symbol 時,會對其進行求值,能夠用'
來表示其字面量
user> (def tom 'tom) ;; 這裏指向的值爲一個 symbol 類型的 tom #'user/tom user> tom tom
有一點很是重要,任何兩個同名的 symbol 是不相等的
user> (identical? 'tom 'tom) false
初次接觸可能會有些疑惑,但也比較好理解,能夠想一想
相同名的變量,在不一樣地方,不一樣時刻具備不一樣的值是再正常不過的了。
並且 symbol 能夠帶有元數據,因此頗有可能同名的 symbol 具備不一樣的元數據。相比之下,關鍵字(keyword)類型因爲其值指向自身,相同名的關鍵字就是同一個對象,因此 keyword 是不容許帶元數據的。
user> (identical? :tom :tom) true user> (with-meta :tom {:some-prop true}) ;; 會報錯 java.lang.ClassCastException: clojure.lang.Keyword cannot be cast to clojure.lang.IObj
最後比較重要的一點,宏傳入的參數都是 symbol 字面量,好比:
user> (def a 1) #'user/a user> (defmacro demo-macro [params] (println params)) #'user/demo-macro user> (demo-macro a) a nil
能夠看到這裏打印的是symbol a
,而不是數字 1。因爲demo-macro
什麼也沒返回,因此在打印出 a 以後輸出了 nil。
因爲 Lisp 採用 s-expression 做爲其語法,因此自然具備 code as data 的特色,也就是說 Lisp 程序自己就是一個 Lisp 數據,能夠像操做其餘數據類型同樣來操做程序自己,這其實就是宏作的事情。老牌 Lisp hacker Paul Graham 在黑客與畫家一書中有提到:
Lisp 並不嚴格區分讀取期、編譯器、運行期。在編譯期去運行就是宏,能夠用來擴展語言。
關於這三個時期的關係,能夠用下面的圖來表示
因爲運行期主要進行的是函數的調用,結合上面 symbol 的知識能夠這麼定義宏
宏是編譯期執行的函數,參數爲 symbol 類型,返回由 symbol 組成的程序(也是數據)。返回值在運行時執行。
定義宏的defmacro
自己也是個宏,能夠將其展開:
user> (macroexpand '(defmacro demo-macro [params] params)) (do (defn demo-macro ([&form &env params] params)) (. #'demo-macro (setMacro)) #'demo-macro)
能夠看到,Clojure 裏面是調用 setMacro
來將宏與通常的函數分開。
關於宏展開(通常不說宏調用)在整個 Clojure 程序生命週期中的位置,能夠參考下圖:
若是對實現細節感興趣,能夠參考我以前的文章:《Clojure 運行原理之編譯器剖析篇》。
對於初學 Lisp 的同窗,對 symbol 類型不是很清楚,究其緣由是這個類型在非 Lisp 語言中是不存在的,爲何不存在呢?主要是由於它們無法像 Lisp 同樣具備 code as data 的特色,有些非 Lisp 語言可能也有所謂的 symbol 類型,像 ruby,可是 DSL 在 ruby 中與 Lisp 是徹底不同的。區別能夠參考下圖
What makes Lisp macros possible, is so far still unique to Lisp, perhaps because (a) it requires those parens, or something just as bad, and (b) if you add that final increment of power, you can no longer claim to have invented a new language, but only to have designed a new dialect of Lisp ; -)
Paul Graham 「What makes Lisp different」