按語:我從懸崖上跳了下去,在除了墜落沒別的事可幹的過程當中爲「不懂編程的人」寫了這一系列文章的第八篇,整理於此。它的前一篇是《周遊抑或毀滅世界》,講述了遞歸函數的基本用法。html
警告,這一篇很長。若不能堅持看到最後,最好仍是別看了。更況且,不看它,也沒啥損失。編程
下面這個函數,能夠對天然數求和:segmentfault
(defun sum (n) (if (= n 0) 0 (+ n (sum (- n 1)))))
傳說數學界的大宗師高斯同窗上小學的時候,曾因飛快地算出從 1 到 100 的天然數之和而震驚四座。網絡
在計算機裏不須要精巧,只須要笨拙,因此上述的 sum
函數就是從大到小,將數字逐一累加。例如:函數
(sum 100)
在 sum
這臺發動機周而復始的運轉中,每次都將本身的參數減去 1,而後與本身下一次的運行結果相加,最終發動機的運轉軌跡造成了 100 + 99 + ... + 1 + 0 這樣的表達式,固然在 Emacs Lisp 裏是這樣的:網站
(+ 100 (+ 99 (+ ... (+ 1 0) ...)))
當 sum
發現本身的參數爲 0 時,它就中止轉動,此時 Emacs Lisp 就獲得了上述的表達式,接下來,Emacs Lisp 解釋器就開始對這個很長的表達式求值,不過就是逐一完成數字累加的這種粗笨的工做,結果爲 5050,跟高斯算出來的同樣,並且可能比高斯算得還要快許多倍。code
不過,這篇文章不是再次重複發動機啊發動機啊的,而是開始思考怎麼去定義一個不知道名字的發動機。htm
再看一遍 sum
函數的定義:blog
(defun sum (n) (if (= n 0) 0 (+ n (sum (- n 1)))))
要讓一個發動機周而復始地運轉,必須得知道它的名字。有了名字,sum
函數方能在本身的定義中對本身進行求值。遞歸
因而,問題就來了。假若宇宙的一切運動是由一臺發動機驅動起來的,那麼這臺發動機是如何運轉的呢?這臺發動機沒有名字,由於它比咱們這些命名者先出現。這臺發動機的存在,暗示着,沒有名字也能周而復始。
所謂函數,本質上就是值與值的映射,它有沒有名字,可有可無,關鍵在於可否在不依賴名字的前提下精確描述映射關係。爲此,Emacs Lisp 語言提供了讓函數匿名的語法——Lambda 表達式。對於函數 f(x, y) = x + y,用 Lambda 表達式可表示爲:
(lambda (x y) (+ x y))
假若 x = 1, y = 2,那麼如何經過這個 Lambda 表達式對 f(x, y) 進行求值呢?像下面這樣作:
(funcall (lambda (x y) (+ x y)) 1 2)
在 Emacs Lisp 程序中,須要藉助 funcall
方能對匿名函數進行求值。在其餘的一些 Lisp 語言裏,可沒必要如此。沒錯,世界上不止一種 Lisp,Emacs Lisp 不過是 Lisp 語言世界裏的一種方言。在 Scheme 這種 Lisp 方言裏,對上述的匿名函數進行求值,只需:
((lambda (x y) (+ x y)) 1 2)
沒有名字,會讓世界變得更混亂一些,不過這反而是世界本來的樣子。從這個角度去探索世界的本原會更爲客觀,可能也更爲奇妙。
當咱們仰望星空,俯瞰大地,感受冥冥中有一種力量在驅動着或者支配着這個世界的運行,咱們無法給這種力量取名,即便取了名,彷佛也沒有用,由於無法根據所取的名字去描述它的運做原理。智慧如老子,也只能勉強給這種力量取個名字,叫做「大」。大,就是沒有邊界。沒有邊界,就是很是遙遠。很是遙遠,就是本身的後腦勺——你向任何一個方向看去,距離你最遠的地方是你的後腦勺。
不能給這種力量取名,就無法描述它的運做原理了嗎?假若世界受一臺沒有名字的發動機的驅動而運轉,那麼咱們隨便從這個世界裏找一臺發動機,而後利用匿名函數消除這臺發動機的名字,結果是否是就可以切實感覺到它的存在呢?
試試看:
(lambda (n) (if (= n 0) 0 (+ n (sum (- n 1)))))
很好,已經消除掉外層的 sum
了,可是裏面的 sum
怎麼消除?這須要藉助一個小技巧,即它雖然在那裏,可是咱們能夠裝做看不見它。就像房間裏有一我的,你不喜歡他,可是又沒有能力讓他消失,因此只好裝做沒看見他。每一個人應該都具有這種技能。那麼,怎樣對上面這個匿名函數裏面的 sum
視而不見呢?把它看成房間裏一種沒有意義的物件就能夠了,像下面這樣:
(lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (thing (- n 1))))))
沒有意義,就是意義不肯定。意義不肯定的東西,就是變量。函數的參數是變量。因此,只須要將 sum
變成一個函數的參數,就至關於對它視而不見處了。
如今已經成功地將全部的 sum
消除了,咱們獲得了一個沒有名字的函數。不過,再仔細看一下,真的獲得了一個沒有名字的函數嗎?在上述函數的定義中,(thing (- n 1))
顯然是一個函數求值表達式,所以咱們不經意間依然將 thing
當成了一個有名字的函數了。要完全的消除名字,必須將 thing
當成一個匿名函數,對它進行求值要藉助 funcall
,所以上述函數應當改爲:
(lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1))))))
注:在一些其餘 Lisp 方言裏,例如 Scheme,不須要這一步。
如今算是完全消除了名字。如今,咱們將這個函數視爲咱們所創造的第一個沒有名字的函數,可是爲了便於描述,姑且將其稱爲 X。對 X 進行求值,須要向它傳遞一個匿名函數,求值結果是一個匿名函數,就這麼奇怪。
X 可以用於天然數序列的求和嗎?試試看:
(funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) ...)
注:SegmentFault 網站的 Markdown 解析器可能有 Bug。以單個字母 + 1 個空格開頭的文本,會被誤認爲是帶編號的列表,從而變成
1. ... ...
。
寫着寫着就發現,在省略號的地方寫不下去了,不知道該向這個函數傳遞什麼樣的參數。雖然咱們很清楚,thing
應該是一個匿名函數,可是咱們如今並無這個函數。所以,不妨試着隨便定義一個,將它做爲 thing
傳給 X,看看會發生什麼:
(funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (lambda (m) m))
新定義的匿名函數就是 (lambda (m) m)
,這個函數什麼也沒作,就是把本身接受的參數做爲求值結果。將它做爲參數傳遞給 X 以後,X 的求值結果就是下面這個匿名函數:
(lambda (n) (if (= n 0) 0 (+ n (funcall (lambda (m) m) (- n 1)))))
假若將 100
傳遞給這個匿名函數,即:
(funcall (lambda (n) (if (= n 0) 0 (+ n (funcall (lambda (m) m) (- n 1))))) 100)
結果獲得 199,而不是 5050。看來這個匿名函數只能算 100 + 99。不要沮喪,由於咱們如今又獲得了一個能夠做爲 thing
傳給 X 的匿名函數了。把它傳給 X:
(funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (lambda (m) m))
如今,獲得的是可以計算 100 + 99 + 98 的匿名函數。
是否是發現了一點玄機了?咱們一開始隨便定義了一個匿名函數,把它傳給 X,結果獲得了一個新的匿名函數,而後再將這個新的匿名函數傳遞給 X。
再試試用一樣的手法, 將可以計算 100 + 99 + 98 的匿名函數傳給 X:
(funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (lambda (m) m)))
結果就獲得了一個能計算 100 + 99 + 98 + 97 的匿名函數。
繼續將能計算 100 + 99 + 98 + 97 的匿名函數傳給 X:
(funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (funcall (lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall thing (- n 1)))))) (lambda (m) m))))
結果就獲得了一個能計算 100 + 99 + 98 + 97 + 96 的匿名函數了。
只要你不怕麻煩,能夠將上述過程繼續下去,最終 X 就能算出 100 + 98 + ... + 1 + 0。最後一次傳給 X 的匿名函數一定是體積極爲龐大的怪物。
爲了更便於理解,咱們用 X 這個名字,將能計算 100 + 99 + 98 + 97 + 96 的匿名函數簡寫成:
(funcall X (funcall X (funcall X (lambda (m) m))))
利用 X 生成匿名函數,再將這個匿名函數傳遞給 X,這個想法很好,只是在實現上有些愚蠢。這個過程相似於爲了實現 1 個發動機運轉 100 圈的效果,製造了 100 個發動機,讓它們的每個只運轉一圈。即便這樣作,也不是太難,可是假若要實現 1 個發動機轉無數圈的效果呢?好辦,製造無數個發動機,讓每個只運轉一圈。假若你真的這樣想,那就對了。製造無數個發動機,每個只運轉一圈,這不就是至關於將 X 做爲參數傳給本身,而後讓它運轉一圈嗎?這個過程應該像下面這樣描述:
(funcall X X)
固然,在 X 的定義中,須要讓做爲參數的 X 運轉一圈,以便生成像 (lambda (m) m)
這樣的匿名函數。所以,將 X 的定義修改成:
(lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall (funcall thing thing) (- n 1))))))
而後按照 (funcall X X)
這樣寫:
(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)))))))
上述表達式的求值結果是一個匿名函數。個匿名函數是什麼呢,就是 sum
函數。不信的話,就用它來計算 0 到 100 的和:
(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)
這個表達式看上去很複雜,把它理解爲 (funcall (funcall X X) 100)
會更清楚。這樣,咱們就構造了一個比上文用 X 構造的匿名函數再傳給 X 的代碼簡潔了將近 100 倍的可對天然數序列求和的匿名函數。
看,咱們沒有使用 defun
,單純藉助匿名函數就實現了一個遞歸函數。這說明了什麼?這說明了,即便這個世界沒有任何名字,它依然可以運轉,亦即世界只能感覺或描述,而不能定義。
一個周而復始的發動機,它之因此可以如此,不是由於它有了名字,而是由於它的本質在於將本身做爲參數傳遞給本身。這就是函數遞歸求值的本質。
雖然我說的天花亂墜,但上面那個表達式其實是沒法求值的,Emacs Lisp 解釋器會報錯,說 thing
符號做爲變量是無效的。這是由於,Emacs Lisp 語言因爲歷史太過於悠久,而 Lisp 的世界裏許多好東西出現的比較晚,爲了兼容過去的程序,致使 Emacs Lisp 解釋器不得不墨守成規。要讓上述表達式可以正確求值,須要在它以前使用下面的語句開啓開啓詞法域模式:
(setq lexical-binding t)
Emacs Lisp 解釋器默認在動態做用域模式下工做,不能正確識別做爲參數傳遞的匿名函數,而詞法域模式卻能夠。關於動態做用域與詞法域,之後再做介紹。
如今,咱們僅實現了一個特定功能的匿名函數的遞歸求值,尚未真正達成咱們的目標,即尋找一個通用的匿名函數遞歸機制。只有這種發動機可以驅動整個世界。不過,咱們彷佛已經有了一個正確的方向,如今要作的就是,繼續前進。
從新觀察 X:
(lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall (funcall thing thing) (- n 1))))))
位於內層的匿名函數是與特定功能相關的部分,咱們能夠用前文已經使用了屢次的手法,將這部分代碼提出來,做爲一個單獨的匿名函數,稱之爲 F,而後想辦法將它做爲參數傳給 X 便可。
F 的定義以下:
(lambda (n) (if (= n 0) 0 (+ n (funcall (funcall thing thing) (- n 1)))))
這個定義是錯的,由於 thing
本來是 X 的參數。以前, F 位於 X 內部,它認識 thing
,如今它脫離了 X,就不認識它了。該怎麼辦呢?用老辦法,凡是本身不喜歡的或者不認識的,通通提高爲參數,裝做沒看見它們……因而,咱們將 F 修改成:
(lambda (thing) (lambda (n) (if (= n 0) 0 (+ n (funcall (funcall thing thing) (- n 1))))))
等會!這東西似曾相識,它不就是 X 麼?沒錯……X 的引力太大,F 彷佛沒法掙脫它而孤立地存在。
再仔細研究一下,F 之因此難以擺脫 X,主要是由於 (funcall (funcall thing thing) (- n 1))
的制約。這是個函數求值表達式,(funcall thing thing)
是一個函數,它接受一個參數。咱們想辦法把它做爲 F 的參數如何?
試試看:
(lambda (thing*) (lambda (n) (if (= n 0) 0 (+ n (funcall thing* (- n 1))))))
爲了與 X 的參數 thing
有所區分,我特地將 F 的參數命名爲 thing*
,而實際上,繼續用 thing
也沒事。
假若咱們對 F 像下面這樣求值:
(funcall (lambda (thing*) (lambda (n) (if (= n 0) 0 (+ n (funcall thing* (- n 1)))))) (lambda (m) (funcall (funcall thing thing) m)))
看上去有點亂,這樣看就比較清楚了:
(funcall F (lambda (m) (funcall (funcall thing thing) m)))
就是向 F 傳遞了一個具備 1 個參數的匿名函數,函數體中的 thing
是 X 的參數,因此咱們能夠將上述表達式扔到 X 的定義裏:
(lambda (thing) (funcall F (lambda (m) (funcall (funcall thing thing) m))))
看,如今 X 裏面再也不有任何特定功能的代碼了。咱們已經成功地將原先的天然數求和的代碼從 X 中分離了出來,這部分代碼構成了匿名函數 F。
如今,咱們再用 (funcall X X)
的辦法對 X 進行求值,即:
(funcall (lambda (thing) (funcall F (lambda (m) (funcall (funcall thing thing) m)))) (lambda (thing) (funcall F (lambda (m) (funcall (funcall thing thing) m)))))
這樣,就能夠獲得一個遞歸的匿名函數,並且這個匿名函數裏面沒有任何特定功能的代碼,它是一個通用的匿名遞歸函數。不過,如今它還不知道 F 是什麼。用老辦法,凡是你不喜歡的或者你不知道的,若你不想由於它們而壞掉好心情,就將它們通通提高爲函數的參數:
(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))))))
咱們將這個函數稱爲 Y。
還記得上面所定義的含有天然數求和代碼的那個 F 嗎?
(lambda (thing*) (lambda (n) (if (= n 0) 0 (+ n (funcall thing* (- n 1))))))
以它爲參數,對 Y 進行求值,即
(funcall (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)))))) (lambda (thing*) (lambda (n) (if (= n 0) 0 (+ n (funcall thing* (- n 1)))))))
看着挺複雜,實際上不過是 (funcall Y F)
,求值結果是什麼呢?一個匿名的天然數序列求值函數。假若不信,那麼用它算一下 0 到 100 的和:
(funcall (funcall (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)))))) (lambda (thing*) (lambda (n) (if (= n 0) 0 (+ n (funcall thing* (- n 1))))))) 100)
結果爲 5050。
上述的匿名函數 Y,因爲它所接受的參數是一個匿名函數。按照數學家們的說法,無論函數是否是匿名的,只要是參數爲函數的函數,就叫算子。因此,函數 Y 應該叫 Y 算子,固然更多的人願意稱它爲 Y 組合子,並將這個算子視爲匿名函數世界中的神蹟。發現這個算子的那個數學家,將這個算子的數學公式 Y = λf. (λx. f(x x)) (λx. f(x x)) 紋在了本身的胳膊上。
這個 Y 組合子,也就是咱們一開始想要尋找的那個驅動整個世界的無名發動機。不過,如今它看上去還有點不夠理想。由於它的參數 F
只是具備 1 個參數的函數,這意味着 Y 組合子構造的匿名遞歸函數只能接受 1 個參數。不過,這不是什麼大問題,多個參數的函數老是能經過單個參數的函數構造出來,不過,這是另一個話題了。
這篇文章可能你看不懂。不要緊,不懂這個,不影響學會編程。相似於不懂汽車發動機原理,同樣能夠駕駛汽車。畢竟咱們是生活在一個處處都充滿了名字的世界裏。彷佛老子早在兩千多年前就看清了這一切,他低吟着:無名,萬物之始也;有名,萬物之母也。故常無慾,以觀其妙;常有欲,以觀其徼……雖然數學家發現了 Y 組合子,但他們也不會真的在編程中使用這種東西來寫遞歸函數。
假若你真的很想看懂這篇文章,惟一的辦法是,集中精力,動手動腦,逐步推導。理解 Y 組合子,並不是毫無心義,將 Y 倒過來,它是我的。這方面的話題,我不想多說,否則會使人反感。
下一篇:長長的望遠鏡
在寫這篇文章時,參考瞭如下網絡文檔: