無所遁形

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

還記得

(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 了,這樣可以讓代碼更清晰一些:

(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)

匿名函數可以作爲參數傳遞給 list-map,那麼有名的函數可不可以?試試看:

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

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

在 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 語言瞭解這方面的一些概念。

下一篇'()

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