無所遁形

按語:我任何路邊的攝像頭下走過的時候爲「不懂編程的人」寫了這一系列文章的最後一篇,整理於此。它的前一篇是《咒語》,介紹瞭如何在 Emacs Lisp 程序的世界裏登壇做法,呼風喚雨。編程

還記得segmentfault

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

麼?函數

當時,爲了表示把手綁起來也能用腳寫字,因此故意沒用 let,如今能夠坦然地用 let 了,這樣可讓代碼更清晰一些:code

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

這個函數能夠將函數 f 做用於 列表 a 中的每一個元素,結果爲一個列表。例如:遞歸

(list-map '(1 2 3) (lambda (x) (+ x 1)))

結果爲 (2 3 4)get

匿名函數能夠做爲參數傳遞給 list-map,那麼有名的函數可不能夠?試試看:io

(defun ++ (x) (+ x 1))
(list-map '(1 2 3) ++)

不行。Emacs Lisp 解釋器抱怨,++ 是無效的變量。它的抱怨沒錯,++ 是個函數,不是變量。雖然在邏輯上,變量與函數不用分得太清,可是 Emacs Lisp 解釋器從形式上分不清什麼是函數,什麼是變量。不過,其餘 Lisp 方言,例如 Scheme 就可以分辨出來。歸根結底,仍是 Emacs Lisp 的年代過於久遠致使。function

在 Emacs Lisp 裏,須要將上述的 list-map 表達式改爲下面這樣:匿名函數

(list-map '(1 2 3) (function ++))

或者簡寫形式:變量

(list-map '(1 2 3) #'++)

function#' 告訴 Emacs Lisp 解釋器,後面這個符號是函數。這樣 Emacs Lisp 就能夠正確識別 ++ 了。

以上,只是本文的前奏。下面咱們來思考一個更深入的問題。這個問題可能深到無止境的程度。

如今,假設 list-map 所接受的列表是一個嵌套的列表——列表中有些元素也是列表:

(list-map '(1 2 3 (4 5 6) 7 8 9) #'++)

對這個表達式進行求值,發現 list-map 失靈了,++ 無法做用於列表元素 (4 5 6)++ 只能對一個數進行增 1 運算,卻不能對一個列表這樣作。假若咱們真的很想讓 ++ 可以繼續進入 (4 5 6) 內部,將其中每個元素都增 1,而後再跳出來繼續處理 (4 5 6) 後面的元素,該怎麼辦?

首先,咱們須要具備判斷列表中的一個元素是否是列表的能力。Emacs Lisp 提供的 listp 函數可讓咱們具備這種能力。例如:

(listp 3)
(listp '[1 2 3])
(listp '(1 2 3))
(listp '())
(listp nil)

上面這五個表達式,前兩個的求值結果皆爲 nil,後面三個的求值結果皆爲 t

有了 listp,咱們就能夠區分一個列表元素是原子仍是列表了。能區分,就好辦。假若列表元素依然是列表,那麼咱們就繼續將 list-map 做用於這個元素,而假若它不是列表,那麼就用 ++ 之類的函數伺候之。

試試看:

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

試驗一下這個新的 list-map 能不能用:

(list-map '(1 2 3 (4 5 6) 7 8 9) #'++)

結果獲得 (2 3 4 (5 6 7) 8 9 10),正確。

再拿更多層數的列表試試看:

(list-map '(1 2 3 (4 5 (0 1)) 7 8 9 (3 3 3)) #'++)

結果獲得 (2 3 4 (5 6 (1 2)) 8 9 10 (4 4 4)),正確。

就這樣,咱們只是對 list-map 略動手腳,彷佛就可讓不管嵌套有多少層,藏匿有多深的列表,在 list-map 面前都是盡收眼底的。

在未對列表結構有任何破壞的狀況下,能夠肯定上述的感受是正確的。由於計算機的運轉老是周而復始。假若程序自己只變更了數據的形狀,而未破壞它的拓撲結構,咱們就老是可以作到見微而知著。

上面對 list-map 的修改,雖然只考慮了再次使用 list-map 來處理列表元素爲列表的狀況,結果卻讓 list-map 可以適用於任何形式的列表嵌套。咱們在用宏的形式定義 my-let* 的時候也遇到過這樣的狀況。爲何會這樣?這實際上是在周而復始的運動中,出現了類型。listp 可以判斷一個值是不是列表類型。在一個 Emacs Lisp 程序裏,能夠有無數個列表,但它們的類型倒是相同的,都是列表類型。

天網恢恢,疏而不漏,靠的不過是遞歸 + 類型。類型,描述了值的共性。它生活在柏拉圖的理想國裏,是一種完美的模具,而那些值只不過是從模具裏鑄出來的東西。類型是比遞歸一個更大的題目,已經有許多人寫了這方面的專著。假若你對這個感興趣,能夠經過 Haskell 語言瞭解這方面的一些概念。

下一篇'()

相關文章
相關標籤/搜索