上一章:庫編程
上一章實現了只定義了一個函數的庫 newbie.el。事實上,這個函數能夠不用定義成函數,定義成宏也能夠,並且能讓調用代碼的執行效率微乎其微地更高一些。由於,調用函數,就像是去車站乘坐客車,而調用宏,猶如乘坐自家的私家車。這是一個不是很準確的比喻,因此它僅僅是個比喻。segmentfault
先定義一個什麼也幹不了的宏,函數
(defmacro foo ())
在形式上,定義宏,彷佛跟定義函數差很少,只是 defun
換成了 defmacro
。code
調用一個宏,也跟調用一個函數差很少,例如調用上述定義的什麼也幹不了的宏 foo
;get
(foo)
對於這個宏調用,Elisp 的求值結果是 nil
。爲何是 nil
呢?由於 Elisp 解釋器遇到宏調用語句,會用宏的定義替換它,此即宏的展開。上述 (foo)
語句會被替換爲class
就是什麼都沒有。什麼都沒有,就是 nil
。效率
假若是讓 foo
的定義有點什麼,例如變量
(defmacro foo () t)
那麼宏調用語句的展開結果就是 t
。語法
宏也能夠像函數那樣擁有參數,例如程序
(defmacro foo (x) x)
宏調用 (foo "Hello world!")
的展開結果即是 "Hello world!"
。
宏的定義,展示的是 Lisp 語言的一個很重要的特性,在程序裏能夠像構造數據同樣地構造程序。例如
(defmacro foo () (list '+ 1 2 3))
Elisp 解釋器會對宏定義裏的表達式予以求值。上述宏定義裏的 (list '+ 1 2 3)
,求值結果就是 (+ 1 2 3)
。所以,宏調用語句 (foo)
會被 Elisp 解釋器展開爲 (+ 1 2 3)
,而後 Elisp 解釋器會對宏的展開結果繼續進行求值,所以 (foo)
的求值結果是 6。利用 Elisp 解釋器對宏的定義和調用的處理機制,即可以在程序裏像構造數據同樣地構造程序。
因爲 (list '+ 1 2 3)
與 '(+ 1 2 3)
近乎等價,所以上述宏定義可簡化爲
(defmacro foo () '(+ 1 2 3))
在宏的定義裏使用引號構造程序要注意引號會屏蔽 Elisp 解釋器對參數的處理。例如
(defmacro foo (x y z) '(+ x y z))
這個宏的定義是合法的,可是若像下面這樣調用它
(foo 1 2 3)
並不會被展開爲 (+ 1 2 3)
,而是會被展開爲 (+ x y z)
。由於 Elisp 在對宏定義求值時,認爲宏定義裏的 '(+ x y z)
只是一個字面意義上的列表,其中的 x
,y
, z
並不是宏的參數值。所以,在宏的定義裏,須要清楚,哪些是字面上的數據,哪些是變量或函數調用。對於上例,須要用回 list
,即
(defmacro foo (x y z) (list '+ x y z))
如此,(foo 1 2 3)
便會被展開爲
(+ 1 2 3)
宏定義
(defmacro foo (x y z) (list '+ x y z))
與
(defmacro foo (x y z) `(+ ,x ,y ,z))
同義。
引號 '
可讓一個列表總體變成字面意義上的列表,而反引號(一般在鍵盤上與 ~
位於同一鍵位)也可讓一個列表變成字面意義上的列表,可是假若前面由 ,
修飾的符號,例如宏的參數,Elisp 解釋器便再也不將其視爲字面意義上的符號了。
在反引號做用的列表裏,,@
可將一個列表裏的元素提高到外層列表,例如
`(1 ,@(list 2 3) 4)
和
`(1 ,@'(2 3) 4)
以及
`(1 ,@`(2 3) 4)
的求值結果皆爲 (1 2 3 4)
。
利用這些奇怪的符號,在宏定義裏像構造構造程序會更爲便捷。
如下代碼定義的宏
(defmacro print! (x) `(progn (princ ,x) (princ "\n")))
可代替 newbie.el 裏的 princ\'
,例如
(print! "Hello world!")
有些時候,須要在宏的定義裏使用局部變量。例如
(defmacro bar (x y a) `(let (z) (if (< ,x ,y) (setq z ,x) (setq z ,y)) (+ ,a z)))
這個宏可將其參數 x
和 y
中較小者與 a
相加。例如
(bar 2 3 1)
求值結果爲 3。
bar
的調用若是出如今一些巧合的環境裏,例如
(let ((z 1)) (bar 2 3 z))
求值結果爲 4,而不是 3。之因此會出現這種不符合預期的結果,是由於上述宏調用語句被展開爲
(let ((z 1)) (let (z) (if (< 2 3) (setq z 2) (setq z 3)) (+ z z)))
之因此會出現這樣的展開結果,是由於 Elisp 解釋器不會對宏參數進行求值,而是將其原樣傳入宏的定義,用它們去替換宏的參數。(bar 2 3 z)
的第三個參數是 z
,Elisp 解釋器將這個參數原樣傳入 bar
的定義後,後者的參數 a
就被換成了 z
,可是 bar
的定義裏有一個局部變量 z
,在最後的 (+ z z)
表達式裏,第一個 z
本應是我傳給 bar
的參數,可是 Elisp 解釋器在這種狀況下,會認爲它是 bar
的局部變量,因而,計算結果便不符合個人預期了。
能保證宏定義裏的局部變量不與宏展開環境裏外部變量產生混淆的宏,稱爲「衛生宏」。Elisp 的宏不衛生。同爲 Lisp 方言的 Scheme 語言提供了衛生宏。近年來,新興的 Rust 語言也支持衛生宏。不過,Elisp 能夠利用體制外(Uninterned)的符號模擬衛生宏。
Elisp 解釋器在對程序解釋執行的過程當中,會維護一些存儲着符號的表,這些符號要麼是綁定了數據,要麼是綁定了函數,要麼是綁定了宏。出如今這些表裏的符號,就是體制內的(Interned),沒出如今這個表裏的符號,就是體制外的。使用 Elisp 函數 make-symbol
能夠建立體制外的符號。例如
(setq z 3) (setq other-z (make-symbol "z"))
第一個表達式裏的 z
是綁定到數字 3 的符號,它是體制內的,而 make-symbol
建立的符號也叫 z
,但它是體制外的,我用一個體制內的符號 other-z
綁定了這個體制外的也叫 z
的符號。利用這個 other-z
綁定的體制外的 z
符號,即可以令上一節定義的宏 bar
變得衛生,即
(defmacro bar (x y a) (let ((other-z (make-symbol "z"))) `(progn (if (< ,x ,y) (setq ,other-z ,x) (setq ,other-z ,y)) (+ ,a ,other-z))))
bar
的新定義不再怕變量捕捉了。試試看,
(let ((other-z 1)) (bar 2 3 other-z))
在上述調用 bar
的語句裏,雖然第三個參數與 bar
定義裏的局部變量 other-z
同名,可是不會再發生變量捕捉的狀況了,於是上述代碼的求值結果爲 3。
從新定義的 bar
是如何避免變量捕捉的呢?要理解這一切,就要對 Elisp 如何對宏的定義進行求值有深入的理解。首先,Elisp 解釋器會對宏定義裏的任何一個表達式進行求值,假若想禁止它對某個表達式求值,那就須要用引號。用引號修飾的表達式,Elisp 解釋器會將其視爲常量。可是,經過反引號以及逗號,能夠在 Elisp 視爲常量的表達式裏開闢一些可變之處,後者即是從新定義的 bar
能避免變量捕捉的關鍵,由於 Elisp 對宏定義的常量部分不會求值,可是常量裏可變的地方會進行求值。這就至關於,在宏定義裏,可讓一段代碼處於「靜止」的狀態,而讓這段代碼裏的部分區域是能夠被 Elisp 解釋器修改爲咱們須要的結果。
bar
的定義裏會本來會發生變量捕捉的語句是
(+ ,a ,other-z)
因爲 other-z
已是在 let
表達式的開頭將其綁定到一個體制外的符號 z
了,因此 Elisp 解釋器在對宏定義求值時,會認爲全部的 ,other-z
視爲(或求值爲)這個體制外的符號 z
,亦即等 bar
調用語句被 Elisp 展開後,符號 other-z
已經不是 other-z
了,而是那個體制外的 z
。在 bar
的定義裏,做爲局部變量的 other-z
絕無可能再與外部同名的變量產生混淆了。這就是 Elisp 語言構造衛生宏的辦法。
事實上,在上述 bar
的定義裏,我根本不必使用 other-z
,徹底能夠像下面這樣定義 bar
:
(defmacro bar (x y a) (let ((z (make-symbol "z"))) `(progn (if (< ,x ,y) (setq ,z ,x) (setq ,z ,y)) (+ ,a ,z))))
在上述代碼的 let
表達式裏,體制內的符號 z
綁定到體制外的符號 z
,而後在後續的代碼裏,,z
皆會被 Elisp 解釋器求值爲體制外的符號 z
,如此一來,如下宏調用語句
(let ((z 1)) (bar 2 3 z))
求值結果符合預期,爲 3。
體制外的,有助於衛生建設。
本章僅介紹了 Elisp 宏最爲淺顯的知識,它真正的用武之地是爲 Elisp 語言定義新的語法(這種方式一般稱爲元編程),而非定義 print!
這種本來就能夠用函數輕易實現的東西。