長長的望遠鏡

按語:我在送孩子去幼兒園的路上爲「不懂編程的人」寫了這一系列文章的第九篇,整理於此。它的前一篇是《無名》,講述瞭如何一步一步「推演」出 Y 組合子。編程

我有個長長的望遠鏡,能一直伸到你的家裏面,你說什麼作什麼,我都能看到。segmentfault

怎樣用 Emacs Lisp 語言描述這樣的事?閉包

(defun bar () x)
(defun foo (x) (bar))
(foo '三原色)

(foo '三原色) 進行求值,會在 Emacs 微緩衝區顯現 三原色dom

foo 函數會對 bar 函數進行求值。bar 函數不接受任何參數的函數,可是它的內部卻憑空出現了一個變量 x。令 bar 不寒而慄的是,當 foo 對它求值時,這個 x 居然有意義的,它的值是符號原子 三原色函數

foo 的內部,bar 函數以爲本身見了鬼。code

這實際上是 Emacs Lisp 的動態域(Dynamic domain)在搞鬼。Emacs Lisp 解釋器對 (foo '三原色) 求值,獲得表達式 (bar),而後它繼續對 (bar) 求值,獲得表達式 x,最後它繼續對 x 求值,結果發現這個 x 是個長長的望遠鏡,從 foo 的窗戶一直伸到了 bar 的家裏,這個望遠鏡的品牌叫 三原色。所以,咱們就在微緩衝區看到了匪夷所思的結果。繼承

當 Emacs Lisp 對一個函數表達式求值時,遇到自由變量時,它就會到一個全局的環境中搜索這個自由變量的值,將這個值做爲自由變量的求值結果,假若找不到,就會報錯,說變量無效。遞歸

動態域的這種特性,對於須要長長的望遠鏡的機構頗有用。不過,對於 bar 函數而言,既然 foo 能把長長的望遠鏡伸過來,就不要放過它:作用域

(defun bar () (setq x '黑暗))
(defun foo (x) (progn (bar) x))
(foo '三原色)

再次對 (foo '三原色) 進行求值,此次結果爲 黑暗。由於 bar 抓住了這個伸到本身家裏的望遠鏡,把它的鏡頭塗黑了。get

動態域,是很古老的變量做用域模型。現代的變量做用域叫詞法域,也叫靜態域。

在詞法域裏,每一個函數都有本身的環境。當函數中出現自由變量時,它就在本身的環境裏搜索變量的值,搜到了就做爲自由變量的求值結果,不然就報錯——變量無效。

詞法域的好處是,沒有人可以將長長的望遠鏡伸到你家裏。看下面的例子:

(setq lexical-binding t)
(defun bar () x)
(defun foo (x) (bar))

(foo '三原色)

再對 (foo '三原色) 求值,就會在微緩衝區報錯,說變量 x 無效。這是由於,在 bar 的環境中,x 是未定義的自由變量,因此無效。雖然 foo 中的 x 是有定義的,但它僅僅是與 bar 中的 x 同名而已,它們是兩個不一樣的變量。

下面的代碼是一個匿名函數的求值表達式:

(funcall (funcall (lambda (thing)
              (lambda (n)
                (if (= n 0)
                    0
                  (+ n (funcall (funcall thing thing) (- n 1))))))
            (lambda (thing)
              (lambda (n)
                (if (= n 0)
                    0
                  (+ n (funcall (funcall thing thing) (- n 1))))))) 100)

在動態域裏,這個函數表達式沒法求值,由於當 Emacs Lisp 解釋器在對這個表達式進行求值時,最終抵達 (funcall thing thing) 的時候,全局環境裏面已經沒有了 thing 的定義。由於,當一個函數的求值結果是匿名函數時,在這個匿名函數被求值時,全局環境已經再也不是它還在母體時的那個樣子了。

例如,Emacs Lisp 解釋器對

(funcall (lambda (thing)
       (lambda (n)
         (if (= n 0)
         0
           (+ n (funcall (funcall thing thing) (- n 1))))))
     (lambda (thing)
       (lambda (n)
         (if (= n 0)
         0
           (+ n (funcall (funcall thing thing) (- n 1)))))))

的求值結果是

(lambda (n)
  (if (= n 0)
      0
    (+ n (funcall (funcall thing thing) (- n 1))))))

這時,這個匿名函數裏的 thing,在這個匿名函數的母體中是有定義的,它就是做爲參數傳入的那個匿名函數,可是 Emacs Lisp 對母體求值結束後,thing 的定義也就同時在全局環境中消失了,所以對於這個剛剛從母體中脫胎而出的匿名函數,thing 變成了一個未定義的自由變量,從而致使 Emacs Lisp 解釋器報錯。

簡而言之,在動態域中,你沒有辦法將匿名函數做爲參數傳給自身。所以,在動態域中,你看不到世界的本原,這樣的世界是一個不肯定的世界。這可能就是爲何早期的 Lisp 機器在運行時常常出故障的主要緣由。

在詞法域裏不會有這樣的問題,由於每一個函數都有本身的環境,而且一個匿名函數從母體脫胎而出的時候,它會對母體的環境有所繼承。這種結構稱爲閉包。

在使用 Emacs Lisp 編程時,用動態域仍是詞法域呢?假若你沒有長長的望遠鏡,或者你對這種望遠鏡深惡痛絕,就用詞法域吧,在程序的開頭添加:

(setq lexical-binding t)

;;; -*- lexical-binding: t -*-

最後,略微介紹一下 setq。以前,咱們只見識過經過函數參數傳遞的變量。setq 能夠將一個符號與一個值或一個匿名函數綁定起來。上面已經見識了它將 lexical-bindingt 綁定了起來。下面的例子展現瞭如何將符號與匿名函數綁定起來:

(setq Y
      (lambda (F)
        (funcall (lambda (thing)
                   (funcall F
                            (lambda (m) (funcall (funcall thing thing) m))))
                 (lambda (thing)
                   (funcall F
                            (lambda (m) (funcall (funcall thing thing) m)))))))
(setq F
      (lambda (thing*)
        (lambda (n)
          (if (= n 0)
              0
            (+ n (funcall thing* (- n 1)))))))

這樣綁定以後,用 Y 組合子構造匿名的遞歸函數會更加簡潔:

(funcall (funcall Y F) 100)

要試驗上述代碼,記得開啓詞法域模式。

下一篇從混亂到有序

相關文章
相關標籤/搜索