咒語

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

咒語,或許是存在的,並且也是有用的。說咒語徹底是騙人的鬼話,不科學,這種心態自己就不夠科學。node

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

(setq x (+ x 1))

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

(++ x)

是否也可以讓 x 增 1 呢?函數

若基於咱們如今的 Emacs Lisp 水平,能夠定義一個 ++ 函數:測試

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

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

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

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

++自增,是咒語嗎?排序

不是。它們是咱們司空見慣的形式,也就是所謂的客觀存在的東西。在 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/...

相關文章
相關標籤/搜索