咒語

按語:我在圍觀茅山道士跳大神的時候爲「不懂編程的人」寫了這一系列文章的第十一篇,整理於此。它的前一篇是《從混亂到有序》,介紹瞭如何用 Emacs Lisp 語言寫一個快速排序程序。

咒語,或許是存在的,而且也是有用的。說咒語完全是騙人的鬼話,不科學,這種心態本身就不夠科學。

下面的 Emacs Lisp 代碼可以讓變量 x 增 1:

(setq x (+ x 1))

假設存在 ++ 這樣的咒語,那麼對 x 念這個咒語:

(++ x)

是否也能夠讓 x 增 1 呢?

若基於我們現在的 Emacs Lisp 水平,可以定義一個 ++ 函數:

(defun ++ (x) (+ x 1))

這個函數可以作爲上述問題的答案。沒錯,Emacs Lisp 的函數命名很自由。只要你高興,中文也行,例如:

(defun 自增 (x) (+ x 1))

大多數 Lisp 方言對變量、函數的命名都很自由。

++自增,是咒語嗎?

不是。它們是我們司空見慣的形式,也就是所謂的客觀存在的東西。在 Emacs Lisp 裏,凡是能用函數描述的東西,都不能稱爲咒語。只有一些具有不尋常的能力的語言,才稱得上咒語。

那麼,

(let ((a 1)
      (b 2)
      (c 3))
  (+ a b c))

是咒語嗎?

是。因爲你沒辦法通過函數實現一個這樣的 let。也許有辦法,只是我的功力不夠,實現不出來。

let 不是 Emacs Lisp 語法的一部分嗎?我認爲不是。因爲即使沒有 let,我們也能通過函數完成等價的功能,即

(funcall (lambda (a b c) (+ a b c)) 1 2 3)

let 只是讓這個功能用起來更方便了而已。

在 Emacs Lisp 語言中,的確有一種像咒語一樣的東西,它叫宏。我們可以通過宏,自己造一個像 let 這樣的東西來用。實際上,在其他一些 Lisp 方言裏,letlet* 都是通過宏的方式定義出來的。

我們將要造的這個東西稱爲 my-let,製造它的目的不是要用它來取代 let,而是瞭解如何念 Emacs Lisp 的咒語,哦,不對,是瞭解如何使用 Emacs Lisp 的宏。

在製造 my-let 之前,需要先解決序對列表的拆分問題,即如何將下面這樣的列表

((a 1) (b 2) (c 3))

拆成兩個列表 (a b c)(1 2 3)

先嚐試拆出 (a b c)

(defun take-var-names (var-list)
  (let ((var-name (car (car var-list))))
    (if (null var-name)
        nil
      (cons var-name (take-var-names (cdr var-list))))))

(take-var-names '(('a 1) ('b 2) ('c 3)))

這個函數要比以前訪問列表每個元素的函數稍微複雜了點。在這個函數裏,不僅訪問了列表的每個元素,而且還從所訪問的元素中提取了信息——每個序對的首元素,並將所提取的信息保存到另一個列表裏。簡而言之,就是在訪問一個列表的過程中,構造了一個列表。

不夠,有點不對。我們的目的是要製造一個 像 letmy-let,所以就不好意思再在某個中間環節使用 let 了。因此,需要用匿名函數來替代 take-var-names 裏的 let 表達式,結果爲:

(defun take-var-names (var-list)
  (funcall (lambda (var-name)
             (if (null var-name)
                 nil
               (cons var-name (take-var-names (cdr var-list)))))
           (car (car var-list))))

(take-var-names '(('a 1) ('b 2) ('c 3)))

用類似的方式,可以抽取出 (1 2 3)

(defun take-var-values (var-list)
  (funcall (lambda (value)
             (if (null value)
                 nil
               (cons value (take-var-values (cdr var-list)))))
           (car (cdr (car var-list)))))

(take-var-values '(('a 1) ('b 2) ('c 3)))

從形式上看,take-var-namestake-var-values 的定義只有一個地方不一樣,其他都一樣。倘若我們能將這些不一樣的地方弄成一樣,那麼就可以將這兩個函數就可以合併成一個了。

怎麼將不一樣的地方弄成一樣呢?還記得我們以前是怎樣將一個不想看到的東西變成沒有的麼?方法是將它提升爲函數的參數。這個方法在這裏依然管用。不一樣的地方,提升爲函數的參數,它們就都一樣了。用這個辦法,去定義一個名字叫 take-some-thing 的函數:

(defun take-something (var-list f)
  (funcall (lambda (x)
             (if (null x)
                 nil
               (cons (funcall f x) (take-something (cdr var-list) f))))
           (car var-list)))

像下面這樣使用 take-something 函數,就可以起到與 take-var-names 同樣的效果:

(take-something '(('a 1) ('b 2) ('c 3)) (lambda (x) (car x)))

下面的表達式則起到與 take-var-values 同樣的效果:

(take-something '(('a 1) ('b 2) ('c 3)) (lambda (x) (car (cdr x))))

倘若再認真思考一下,不難發現,現在 take-something 的能力似乎已經遠遠超越了從一個序對列表中提取部分信息的功能,它能夠將一個列表映射爲另一個列表,而且這種映射還很廣義。例如:

(take-something '(1 2 3) (lambda (x) (+ x 1)))

結果可以得到 (2 3 4),即讓列表中每個元素增 1,而這種運算顯然與提取什麼信息似乎沒有關係,因此 take-something 這個函數名需要修改一下,讓它名副其實。就叫它 list-map 吧,即:

(defun list-map (a f)
  (funcall (lambda (x)
             (if (null x)
                 nil
               (cons (funcall f x) (list-map (cdr a) f))))
           (car a)))

它的功能將一個列表 a 映射爲另一個列表,映射規則是 ff 可以將 a 中的一個元素映射爲另一個元素。

有了 list-map,就可以製造 my-let 了,不就是將一個序對列表拆成兩部分,一部分扔給匿名函數作爲參數列表,另一部分扔給匿名函數作爲參數值嗎?假設序對列表爲 bindings,下面的代碼似乎就能夠輕鬆解決這個的問題:

(funcall
 (lambda (list-map bindings (lambda (x) (car x))))
 (list-map bindings (lambda (x) (car (cdr x)))))

應當注意,我們是在製作一條咒語。這條咒語裏的文字是不能以它們在現實世界裏的含義進行解釋的,也就是說,我們要禁止 Emacs Lisp 解釋器對這條咒語中的任何一部分有所解讀。有一個符號能夠起到這種效果,即反引號(很抱歉,我用了 Markdown 標記語言寫的這份文檔,在 Markdown 的普通文本里是沒法給你看反引號的樣子),倘若你的鍵盤很大衆化,反引號與 ~ 符號位於同一個鍵位。現在,將反引號作用於上述代碼:

`(funcall
  (lambda (list-map bindings (lambda (x) (car x))))
  (list-map bindings (lambda (x) (car (cdr x)))))

現在,上述表達式實際上是一個列表,你可以嘗試在 Emacs 裏對它試着進行求值,結果可以得到這個列表的字面形式。

實際上,反引號與 ' 的功能相似,就是告訴 Emacs Lisp 解釋器不要對列表本身以及列表中的任何一個元素進行求值,只不過 ' 太過於武斷,它徹底屏蔽了 Emacs Lisp 解釋器對列表的影響,而反引號允許開後門,讓 Emacs Lisp 解釋器能夠對列表中的部分元素進行求值。要開這個後門,也需要一個符號,即 ,

對於 (list-map bindings (lambda (x) (car x)))(list-map bindings (lambda (x) (car (cdr x)))) ,一定是要開後門的,否則它們就會在字面上變成匿名函數的參數名與參數值,這不是我們想要的結果。現在爲上述代碼加上 ,

`(funcall
  (lambda ,(list-map bindings (lambda (x) (car x))))
  ,(list-map bindings (lambda (x) (car (cdr x)))))

不過,這個匿名函數所接受的參數,形式上不正確。因爲 (list-map bindings (lambda (x) (car (cdr x)))) 的求值結果是一個列表,而匿名函數需要的不是列表,而是脫去列表括號的一組值。不要擔心,Emacs Lisp 提供了 @ 符號,它可以將列表裏的元素取出並平鋪開來:

`(funcall
  (lambda ,(list-map bindings (lambda (x) (car x))))
  ,@(list-map bindings (lambda (x) (car (cdr x)))))

現在,my-let 的匿名函數的參數問題算是得到很好的解決,現在,補上它的身體:

`(funcall
  (lambda ,(list-map bindings (lambda (x) (car x))) ,body)
  ,@(list-map bindings (lambda (x) (car (cdr x)))))

沒錯,也得爲 body 開個後門,否則 Emacs Lisp 解釋器會認爲 body 是個符號原子,而不是一個表達式,而匿名函數的身體必須得是表達式纔可以。

最後,告訴 Emacs Lisp 解釋器,這個東西是咒語,哦,宏:

(defmacro my-let (bindings body)
  `(funcall
    (lambda ,(list-map bindings (lambda (x) (car x))) ,body)
    ,@(list-map bindings (lambda (x) (car (cdr x))))))

大功告成!試着用一下:

(my-let ((a 1)
         (b 2)
         (c 3))
        (+ a b c))

結果等於 6。一切都沒毛病,咒語很管用。

Emacs Lisp 解釋器對宏表達式進行求值時,發生了什麼呢?首先,它將宏按字面展開,不過在這個過程中,它也會對留出後門的表達式進行求值;然後對宏的展開結果進行求值。

使用 macroexpand 函數,可以看到宏的展開結果。例如:

(macroexpand '(my-let ((a 1) (b 2) (c 3)) (+ a b c)))

對上述表達式求值,結果會在微緩衝區或 *Messages 緩衝區裏顯示宏的實際展開結果,即:

(funcall (lambda (a b c) (+ a b c)) 1 2 3)

這個結果,與我們在前面爲 let 表達式構造的等價匿名函數表達式絲毫不差。

真的沒毛病嗎?倘若咒語念得不夠好,經常會失靈。my-let 看上去念的還行。但是,一些複雜的咒語,能念好的人不太多。常見的唸錯咒語的方式可參考 [1]。

接下來,要不要再製作一個 my-let* 去挑戰一下 let*

身爲勤勞勇敢的中國人,在日益增長的美好生活需要和不平衡不充分的發展之間的矛盾面前,當然要響應黨和國家的號召,繼續前進。不知道這樣肉麻,人民日報會不會刊登這篇文章啊。

先回顧一下 let* 的特點,它的特點是在變量綁定列表中允許一個變量的值是前面的變量構成的表達式。例如:

(let* ((a 1)
       (b 2)
       (c (+ a b)))
  (+ a b c))

與這個表達式等價的匿名函數表達式可寫爲:

(funcall (lambda (a)
           (funcall (lambda (b)
                      (funcall (lambda (c) (+ a b c)) (+ a b))) 2)) 1)

看到這樣壁壘森嚴的匿名函數表達式,雙腿難免有點乏力。不過,把這個表達式的形狀略微調整一下,會更清楚:

(funcall (lambda (a)
           (funcall (lambda (b)
                      (funcall (lambda (c) (+ a b c))
                               (+ a b)))
                    2))
         1)

看到了吧,不過是將 list-mapbindings 裏拆分出來的兩個列表分別扔到三重臺階上。

試着先往第一層與最後一層上扔第一個參數與它的值:

(defmacro my-let* (bindings)
  (my-let ((names (list-map bindings (lambda (x) (car x))))
              (values (list-map bindings (lambda (x) (car (cdr x))))))
             `(funcall (lambda (,(car names)))
                       ,(car values))))

如何知道這個宏是不是正確呢?試着將宏表達式代入 macroexpand 函數:

(macroexpand '(my-let* ((a 1) (b 2) (c (+ a b)))))

對上述表達式求值,得到的展開結果爲:

(funcall (lambda (a)) 1)

正確無誤。

接下來,試着繼續試着往第二層與倒數第二層上扔第二個參數與它的值:

(defmacro my-let* (bindings)
  (my-let ((names (list-map bindings (lambda (x) (car x))))
           (values (list-map bindings (lambda (x) (car (cdr x))))))
          `(funcall (lambda (,(car names))
                      (funcall (lambda (,(car (cdr names))))
                               ,(car (cdr values))))
                       ,(car values))))

再次用 macroexpand 對宏進行展開,結果得到:

(funcall (lambda (a) (funcall (lambda ...) 2)) 1)

結果似乎依然正確,由於 macroexpand 在第二層匿名函數裏輸出了省略號,所以也不確定省略號是不是包含了參數名 b 。先不管了,繼續處理第三層與倒數第三層,不過,這次我們需要增加 body——匿名函數的終點:

(defmacro my-let* (bindings body)
  (let ((names (list-map bindings (lambda (x) (car x))))
           (values (list-map bindings (lambda (x) (car (cdr x))))))
          `(funcall (lambda (,(car names))
                      (funcall (lambda (,(car (cdr names)))
                                 (funcall (lambda (,(car (cdr (cdr names))))
                                            ,body)
                                          ,(car (cdr (cdr values)))))
                               ,(car (cdr values))))
                    ,(car values))))

現在可以測試 my-let* 的定義是否正確,下面是測試代碼:

(my-let* ((a 1)
          (b 2)
          (c (+ a b)))
         (+ a b c))

結果爲 6,正確。

不過,這個正確是以大量的重複代碼來保證的。在示例中,僅僅三個參數構成的 bindings 就已經產生這麼臃腫的宏定義了,若是參數更多一些,豈不會把定義宏的人累死嗎?

一定是思路出現了問題。我們需要從頭再來。從最簡單的情況開始。大部分時候,當我們的思路實在很難進展下去的時候,往往是在思路的源頭就出現了偏差。

最簡單的情況是什麼?是 my-let* 的第一個參數爲 nil(空表)的時候,即:

(defmacro my-let* (bindings body)
  (if (null bindings)
      body
    (...)))

上述代碼意味着,倘若 bindingsnil 時,my-let* 的展開結果是 body。省略號部分表示 my-let* 第一個參數不爲 nil` 的情況,然而現在我們還不知道怎麼去寫。

再來看 my-let* 第一個參數可能爲 nil 也可能爲只包含一個序對的情況,對於這種情況可以像下面這樣處理:

(defmacro my-let* (bindings body)
  (if (null bindings)
      body
    (my-let ((x (car bindings)))
            `(funcall
              (lambda (,(car x)) ,body)
              ,(car (cdr x))))))

bindings 只包含一個序對時,匿名函數必須出現 body,而這正是 bindingsnil 時的結果。因此,上述代碼可以修改爲:

(defmacro my-let* (bindings body)
  (if (null bindings)
      body
    (my-let ((x (car bindings)))
            `(funcall
              (lambda (,(car x)) (my-let* ,(cdr bindings) ,body)) ,
              (car (cdr x))))))

於是,奇蹟就出現了,我們已經成功的完成了 my-let* 宏的定義!天下難事,必作於易。天下大事,必作於細。

試試看:

(my-let* ((a 1)
          (b 2)
          (c (+ a b)))
         (+ a b c))

結果爲 6,正確!

我們是怎麼成功的呢?不妨看看 macroexpandmy-let* 的展開結果:

(macroexpand '(my-let* ((a 1)
                        (b 2)
                        (c (+ a b)))
                       (+ a b c)))

結果爲

(funcall (lambda (a) (my-let* (... ...) (+ a b c))) 1)

看到了吧,在 my-let* 的展開結果中又出現了 my-let*,接下來 Emacs Lisp 解釋器不得不繼續對它繼續進行展開,但是這次 my-let 的參數變成了 (cdr bindings)。依此類推,結果就形成了宏的遞歸,直至 bindingsnil,最後一次的 my-let* 展開結果就是 body

沒錯,Emacs Lisp 宏是可以遞歸的。也就是說,宏也能構成周而復始的發動機。

現在,想必你已經大致上對 Emacs Lisp 宏有一定的認識了。它的行爲與函數有些相似,但是二者有着本質的不同。函數操縱的是表達式的值,而宏操縱的是表達式。

在 Emacs Lisp 的世界裏,能駕馭宏的人,他們就像大法師一樣,吞雲吐霧,上天入地,無所不能。

下一篇無所遁形


[1] https://www.gnu.org/software/...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章