按語:我在送孩子去幼兒園的路上爲「不懂編程的人」寫了這一系列文章的第九篇,整理於此。它的前一篇是《無名》,講述瞭如何一步一步「推演」出 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-binding
與 t
綁定了起來。下面的例子展現瞭如何將符號與匿名函數綁定起來:
(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)
要試驗上述代碼,記得開啓詞法域模式。
下一篇:從混亂到有序