按語:我在一鍋湯裏爲「不懂編程的人」寫了這一系列文章的第十篇,整理於此。它的前一篇是《長長的望遠鏡》,做爲《無名》一篇的補充,介紹了 Emacs Lisp 的動態域與詞法域。編程
警告,這一篇雖然很長,不過,基本上想看到哪就看到哪,隨便看點,都算是對 Emacs Lisp 又多了一點了解。segmentfault
有這麼一個列表:ide
(list 3 1 0 8 10 9 7 5 4 999 30)
因爲列表中都是數字原子,沒有須要求值的列表,所以也能夠將它寫成函數
'(3 1 0 8 10 9 7 5 4 999 30)
咱們一眼能夠看出,這個列表中的數字原子的排列沒有順序。假若咱們很在乎這個列表,它就可能會讓咱們有點不舒服。咱們更願意看到像測試
'(0 1 3 4 5 7 8 9 10 30 999)
或者設計
'(999 30 10 9 8 7 5 4 3 1 0)
這樣的列表。咱們認爲這樣的列表是有序的,更易於記憶。不妨試驗一下,用 7 秒的時間可能很難記住上面的那個沒有順序的列表,可是用一樣的時間很容易記住有序的列表。code
用 Emacs Lisp 語言如何編寫一個可以將混亂的數字列表轉化爲有序的列表的程序呢?對象
任何排序,第一步是給要排序的對象創造一個有序的空間,並且保證可以瞬間訪問或修改這個空間的任一位置上的元素。在 Emacs Lisp 裏,這樣的空間稱爲向量。排序
像使用 list
函數構造列表那樣,vector
能夠構造向量:遞歸
(vector 1 2 3 (+ 5 4) (list 1 2 3))
這個表達式的求值結果爲:
[1 2 3 9 (1 2 3)]
這是一個由數字原子與列表構成的向量。
要訪問向量中的任一位置上的元素,可使用 aref
函數。例如:
(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) 4)
能夠訪問向量的第 5 個位置的元素,結果爲列表 (1 2 3)
。
aref
接受的參數是 4,爲何能夠訪問向量的第 5 個位置呢?這是由於向量的位置編號是從 0 開始的,因此編號爲 4 的位置是第 5個位置。
務必記住,向量的位置編號是天然數,而天然數是從 0 開始。
下面這個表達式
(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) -1)
它的求值結果是什麼?Emacs Lisp 解釋器會報錯,說參數超出向量的範圍。由於向量的位置編號是天然數,而天然數裏沒有負數。
下面這個表達式
(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) 5)
它的求值結果是什麼?Emacs Lisp 解釋器會報錯,說參數超出向量的範圍。由於這個向量裏面只有 5 個元素,沒有第 6 個。
務必記住,訪問向量中的元素,不能超出向量的長度。
要肯定一個向量有多長,能夠用 length
函數。例如:
(length (vector 1 2 3 (+ 5 4) (list 1 2 3)))
結果爲 5。
注:
length
也能用於肯定一個列表的長度。
向量中某個位置上的元素不只可以訪問,也能修改,須要使用 aset
函數。例如,將上述向量的第 3 個位置上的元素修改成布爾值 t
:
(setq v (vector 1 2 3 (+ 5 4) (list 1 2 3))) (aset v 2 t) v
對向量 v
的求值結果爲 [1 2 t 9 (1 2 3)]
。
在不知道向量中具體包含哪些元素,只知道向量的長度以及向量元素的類型時,可使用 make-vector
函數構造向量。例如:
(make-vector 7 0)
求值結果爲 [0 0 0 0 0 0 0]
。上述的參數值 0
是向量元素的初始值,可根據須要自行選擇其餘類型的值。
要對一個只包含數字原子的列表進行排序,須要將列表中的元素放入與列表等長度的向量裏面。如今終於有了一個有助於咱們深入理解列表的好機會。
首先,創造與列表等長度的向量:
(make-vector (length a-list) 0)
有了這樣的向量,就能夠將列表中的元素一個一個放進向量裏:
(defun list-to-vector (src dest n) (if (not src) dest (progn (aset dest n (car src)) (list-to-vector (cdr src) dest (+ n 1)))))
這個函數定義裏出現了幾個以前從未提到的函數,car
、cdr
以及 not
。
對於任何列表訪問任務而言,它們是最基本的函數。car
能夠訪問列表的第一個元素,cdr
能夠訪問列表的第一個元素以後的部分。假若將列表比喻爲毛毛蟲,那麼 car
能夠訪問毛毛蟲的頭部,而 cdr
則能訪問毛毛蟲的身體。
not
函數用於對一個布爾值取反,即將真(t
)變成假(nil
),將假變成真。在上述函數的定義中,用 (not src)
來測試 src
是否爲空的列表(即 nil
或 '()
)。這樣作,雖然沒有錯誤,可是卻有些彆扭,更地道的辦法是 (null src)
。
將 car
、cdr
以及 null
鏈接到一個發動機上,就能夠訪問整個鏈表:
(defun 周遊列表 (列表) (if (null 列表) <求值結果> (progn <(car 列表) 參與的運算> (周遊列表 (cdr 列表)))))
在這個發動機的運轉過程當中,它接受的參數是一個在 cdr
函數做用下不斷縮小的列表,在它的內部則由 car
函數取出的列表首元素能夠參與各類運算。上述的 list-to-vector
就是這種形式的發動機。
上述的代碼片斷
(if (null 列表) <求值結果>
用於斷定對列表的訪問過程是否終止。之因此要做這種形式的判斷,是由於 周遊列表
的求值邏輯是每次取出列表的首元素以後,就讓剩餘元素參與下一輪的運算,這個過程至關於 周遊列表
每次輪迴都會「砍掉」列表的首部元素,只要列表的長度有限,終將會出現無首部元素可砍的空表,而對一個空表取反,結果爲真。
無首部元素可砍的表,其實是一個空表,即 '()
,它與 nil
等價,而 (null nil)
的結果一定爲真。因此,對於一個有限長度的列表,在 car
、cdr
以及一個周而復始的函數的做用下,老是能夠用上述的條件表達式來斷定這個周而復始的過程是否應當就此終止。
能夠用下面的語句驗證 list-to-vector
函數是否能正確運行:
(setq src '(3 1 0 8 10 9 7 5 4 999 30)) (setq dest (make-vector (length src) 0)) (list-to-vector src dest 0)
求值結果應該是 [3 1 0 8 10 9 7 5 4 999 30]
。
看一下上一節最後出現的語句:
(setq src '(3 1 0 8 10 9 7 5 4 999 30)) (setq dest (make-vector (length src) 0)) (list-to-vector src dest 0)
setq
將 src
與一個列表進行了綁定,將 dest
這與一個向量進行了綁定。src
與 dest
都是符號,而 setq
彷佛能夠將它們與任何一種表達式進行綁定。所以,咱們能夠用有限的符號去綁定的無限多的表達式。
從效用上來看,這些符號與函數所接受的參數類似。既然咱們能夠將函數的參數視爲變量,就應該將這些符號也視爲變量纔對。再者,因爲函數自己能夠做爲參數傳遞給其餘函數,這意味着函數其實也是變量。定義一個函數,本質上不過是 defun
將一個符號與一個表達式綁定了起來。
變量與函數,不必分得太過於清楚。一個符號,與任意一個表達式存在綁定關係,那麼這個符號就是變量。setq
的工做就是將更換一個符號的綁定對象。
從如今開始,除了定義函數以外,我會將符號與表達式綁定起來的這種行爲稱爲定義變量。
Emacs Lisp 語言具備更緊湊的變量定義語法——let 表達式。它的用法能夠經過改寫上述的三個分離的表達式得以充分體現,即:
(let ((src '(3 1 0 8 10 9 7 5 4 999 30)) (dest (make-vector (length src) 0))) (list-to-vector src dest 0))
let
表達式的通常性結果以下:
(let (<變量 1> <變量 2> <.....> <變量 n>) <使用上述變量的表達式>)
不過,上面的 let
表達式是沒法求值的。由於在定義 dest
變量時,它引用了變量 src
,而 src
的定義必須在 let
語句的第一個表達式求值時纔是一個有定義的變量。這就是說,你不能左腳踩着右腳或右腳踩着左腳來提高本身。
將上述代碼中的 let
改爲 let*
就能夠了。務必記住,當變量定義列表中不存在變量之間的引用時,用 let
,不然用 let*
。
爲啥不所有使用 let*
呢,由於它作功比 let
多,更耗能。
使用 let
/let*
的意義在於,能夠將一組變量彙集到一塊兒,放在一個局部的環境裏。這樣,在這個局部環境以外,即便有存在同名的變量,它們也與這個局部環境內部的變量無關。
let
/let*
彷佛具備一種神祕的力量,但其實它們的原理很是簡單。像下面這樣的 let
表達式:
(let ((a 1) (b 2) (c 3)) (+ a b c))
它其實是
(funcall (lambda (a b c) (+ a b c)) 1 2 3)
像下面這樣的 let*
表達式:
(let* ((a 1) (b 2) (c (+ a b))) (+ a b c))
它其實是
(funcall (lambda (a) (funcall (lambda (b) (funcall (lambda (c) (+ a b c)) (+ a b))) 2)) 1)
至於 Emacs Lisp 解釋器是如何將 let
/let*
變換成上述的匿名函數形式的,這須要瞭解 Emacs Lisp 宏。關於宏,這須要一篇專門的文章來說述它。
通過一番跋涉,見識了 Emacs Lisp 世界裏的一些景觀,如今再回到一開始的問題:將混亂的數字列表轉化爲有序的列表。
經過 list-to-vector
函數,咱們可以將一個列表轉化爲與它等長度的向量。所以,如今的問題能夠轉化爲讓向量變得有序。之因此要作這樣的轉化,是爲了讓程序更省功。經過上文所介紹的訪問列表中每個元素的方法,想必你已經看到了,要訪問列表中的某個元素,一般須要 周遊列表
函數運轉屢次,才能找到那個元素,然而要訪問向量中的任意一個位置上的元素,卻能夠瞬間完成。因爲在排序過程當中,免不了頻繁訪問一些元素,在這方面向量遠勝列表。
怎樣對向量裏面的數字原子進行排序呢?
不要着急。在計算機裏編程,對一組數字進行排序,這是個很大的問題。尚健在的計算機科學界的大宗師 Knuth 老先生在他的傳世著做《計算機程序設計藝術》的第 3 卷裏,專門用了一章全面地討論了這個問題。大宗師寫的東西,通常人是看不懂的。我剛好也可能和你同樣,都是通常人。
複雜的看不懂,就本身去琢磨一些簡單的方法吧。從順序是什麼開始思考。在我看來,所謂數字的順序就是將一個數字放到一個恰當的位置上,使得位於它左邊的數字不大於它,而位於它右邊的數字大於它。
對於向量 [3 1 0 8 10 9 7 5 4 999 30]
,隨便從它裏面取出一個數字,例如它的首元素 3。向量裏面剩餘的數字,要麼比 3 大,要麼不大於 3 小,假若咱們將全部不大於 3 的數字通通放在 3 的左面,而將全部大於 3 的數字放在 3 的右面,那麼就能夠認爲 3 這個數字已經位於它應該在的位置上了。接下來,咱們用一樣的辦法去處理位於 3 的左側的數字與右側的數字就能夠了。
「一樣的辦法」,看到這幾個字,咱們永遠應該馬上首先想起的是製造一個周而復始的發動機——遞歸函數。這個遞歸函數能夠像下面這樣實現:
(defun sort (x begin end) (if (>= begin end) x (let* ((pivot (divide x begin end begin))) (progn (sort x begin pivot) (sort x (+ pivot 1) end)))))
它接受三個參數,參數 x
是待排序的向量,begin
是向量的起始位置,end
是向量的終止位置。在函數遞歸求值過程當中,begin
與 end
用於標定向量子域的起始位置與終止位置。
遞歸求值過程的終止條件是待排序的向量只含有一個元素或向量爲空。例如,假若比 3 小的數字只有一個,則無需對 3 的左側再進行排序了。同理,假若比 3 大的數字只有一個,那麼 3 的右側數字也不必再進行排序。
寫一個遞歸函數,可能許多人是像我上面所說的那樣思考的,即如今腦子裏構造了一個一層又一層深刻下去的函數求值過程,而後思考這個過程總得有個終點,因而他們就開始思考這個終止條件是什麼。之前,我也是這樣思考遞歸的。許多人所以也就以爲遞歸這種東西,彷佛只可意會,不可言傳。當我將函數的遞歸求值過程想象爲一個周而復始運轉的發動機時,我想明白了這個問題。不是遞歸不可思議,上面所說的那種思考模式,其實是上帝視角。由於任何遞歸,在上帝面前(假設真的有這種東西),都是盡收眼底的。當咱們企圖開啓上帝視角來理解遞歸,就變成了要在腦子裏模擬這個遞歸過程許屢次,甚至還有人須要在紙上一層一層的把遞歸過程展開個三五層,而後越看越混亂……咱們不是上帝,因此就不要勉強本身。
實際上,採用 POV(Point of View) 視角來理解遞歸,會更爲直觀。這種視角就是從最簡單的狀況開始,往前走兩步步看看,一旦發現出現了重複,就意味着遞歸出現了。
對於 sort
函數而言,最簡單的狀況是什麼呢?是它接受的向量 x
不包含任何元素,即空向量,或者只含有 1 個元素。對於這樣的向量,就不必排序了,直接將 x
原封不動地做爲排序過程的求值結果便可。很容易爲這種狀況寫代碼,即:
(defun sort (x begin end) (cond ((>= begin end) x)))
因爲 begin
與 end
分別是 x
的第一個元素與最後一個元素的位置,所以只要 (>= begin end)
爲真,向量就一定是空向量或只有 1 個元素的向量。
如今,來看向量中含有兩個元素的狀況。按照上面所述的排序方法,只須要將 x
的首元素調整到一個合適的位置,讓它左邊的元素都比它小或與它相等,而右邊的元素都比它大就能夠了。設這個向量是 [a b]
。將 divide
函數做用於它,x
無非變成 [a b]
與 [b a]
這兩種形式之一。不管是哪種,反正 a
的位置被固定下來了,接下來的問題是分別對 a
左側與右側的元素進行排序,這至關於對 x
的兩個子集進行排序,而 a
所在的位置正是這兩個子集的分割點。這兩個子集,所包含的元素數量一定不大於 1,而不大於 1 的向量的排序問題,咱們已經有了解決方案,只須要將那個已有的方案拿出來用就是了,這樣,遞歸就出現了,即:
(defun sort (x begin end) (cond ((>= begin end) x) ((= (- end begin) 1) ((let ((pivot (divide x begin end begin))) (progn (sort x begin pivot) (sort x (+ pivot 1) end)))))))
其中 pivot
就是上面所說的將 divide
做用於 x
以後,所造成的分割點,基於這個分割點能夠將 x
分爲兩個部分,而後交給 sort
函數對它們進行排序。
如今,咱們繼續考慮向量中含有 3 個元素的狀況,結果發現,處理過程與只有 2 個元素的狀況徹底一致,這就意味着不必再費勁了,接下來含有 4 個、5 個……n 個元素的向量,也都是這樣處理,所以可將 sort
函數修改成:
(defun sort (x begin end) (cond ((>= begin end) x) (t ((let ((pivot (divide x begin end begin))) (progn (sort x begin pivot) (sort x (+ pivot 1) end)))))))
接下來,再將 cond
表達式換成 if
表達式,這就與以前的 sort
徹底同樣了。
經過這種方式去定義遞歸函數,必定要在解決了最簡單的狀況以後,迅速變得足夠懶惰,這樣很容易發現遞歸第一次出現的蹤影,發現了就抓住它。這其實正是咱們慣常使用的思考方式。還記得守株待兔的故事吧,即:宋人有耕田者。田中有株,兔走觸株,折頸而死。因釋其耒而守株,冀復得兔。兔不可復得,而身爲宋國笑。這個宋國的農民太過於着急了,至少得再等到 2 天看看還能不能在原來的地方撿到死兔子,再考慮將待兔做爲職業。這種思考遞歸的方法其實咱們好久之前就學過,數學概括法。
真正有些麻煩的是 divide
函數的實現。divide
函數應當以向量 x
的首個元素爲樞紐,將比這個元素小的元素旋轉到向量的左部,而將比這個元素大的元素旋轉到向量的右部,而且將樞紐所在的位置做爲求值結果。
假若你曾經玩過撲克牌,不妨將 divide
函數視爲洗牌的擬過程。最多見的洗牌方法是雙手各執一組牌,而後讓它們交錯合併爲一組。divide
就是將合併後的牌再從新分開,只不過是以第 1 張牌爲樞紐,讓牌面小的圍繞樞紐向左旋轉,這樣牌面大的就天然出如今樞紐的右邊了。
下面是 divide
的實現:
(defun divide (x i end location) (if (> i end) location (let* ((pivot (aref x location)) (xi (aref x i)) (next (+ location 1))) (if (> pivot xi) (progn (if (> i next) (aset x i (aref x next))) (aset x location xi) (aset x next pivot) (divide x (+ i 1) end next)) (divide x (+ i 1) end location)))))
感受語言有點兒無力。divide
函數其實很機械,它充分利用了 Emacs Lisp 的向量的跨函數的可修改性。它的主體部分是對 x
中的元素的順序訪問過程,只是在這個過程當中對 x
進行了修改,變相地達到了「讓牌面小的圍繞樞紐向左旋轉,這樣牌面大的就天然出如今樞紐的右邊」的效果。例如將向量 [3 1 0 7 5 2]
傳遞給 divide
函數,即 (divide '[3 1 0 7 5 2] 0 5 0)
,這個向量會被修改爲 [1 0 2 3 5 7]
,而且 divide
會返回元素 3
的所在的位置。
下面是測試 sort
可否工做的代碼:
(let ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30)) (dest (make-vector (length src) 0))) (list-to-vector src dest 0)))) (sort x 0 (- (length x) 1)))
結果獲得 [0 1 3 4 5 7 8 9 10 30 999]
。
如今,咱們已經解決了向量的排序問題,而咱們最初要解決的問題是列表的排序。所以,還須要將有序的向量轉換爲有序列表纔算得上功德圓滿。不過,假若你可以理解上述的所有代碼,這種問題對你而言基本上算不上問題。
試試看:
(defun vector-to-list (src i end dest) (if (= i end) dest (
居然寫不下去了。由於咱們還不知道怎樣基於向量中的元素逐一添加到一個列表裏。
Emacs Lisp 爲構造列表提供的最基本的函數是 cons
,它能夠將一個元素添加到列表的首部。例如:
(cons 0 '(1 2 3))
求值結果爲 (0 1 2 3)
。
要構造只含有 1 個元素的列表,也是能夠的,例如 (cons 1 '())
,求值結果爲 (1)
。
如今,能夠繼續寫出 vector-to-list
了,
(defun vector-to-list (src i end dest) (if (= i end) dest (vector-to-list src (+ i 1) end (cons (aref src i) dest))))
如下代碼可驗證這個函數的正確性:
(let ((src '[1 2 3]) (dest '())) (vector-to-list src 0 2 dest))
不過,vector-to-list
目前是將一個升序的向量轉化爲一個降序的列表。假若但願所得列表也是升序,須要將這個函數定義爲:
(defun vector-to-list (src i dest) (if (< i 0) dest (vector-to-list src (- i 1) (cons (aref src i) dest))))
如下代碼可驗證其正確性:
(let ((src '[1 2 3]) (dest '())) (vector-to-list src 2 dest))
將列表轉化爲向量,再對向量進行排序,最後將向量轉化爲列表,這個過程如今能夠用下面的代碼來描述:
(let* ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30)) (dest (make-vector (length src) 0))) (list-to-vector src dest 0))) (end (- (length x) 1))) (vector-to-list (sort x 0 end) end '()))
如今已經完全的解決了這篇文章開始所提出的那個問題,可是想必你也看到了,最後寫出來的那段代碼
(let* ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30)) (dest (make-vector (length src) 0))) (list-to-vector src dest 0))) (end (- (length x) 1))) (vector-to-list (sort x 0 end) end '()))
看上去像個醜陋的怪物。可能到如今,你還沒搞明白 vector-to-list
的第二個與第三個參數的含義吧?還有 sort
函數的第二個與第三個參數……這些參數,就像一部電器裸露在外的一些混亂的電線同樣,使人生厭或生畏。
咱們能夠把它們隱藏起來。隱藏一些東西,最簡單的辦法爲給它製做一個外殼。例如,能夠像下面這樣,將 sort
函數裸露在外的電線隱藏起來:
(defun vector-sort (x) (let ((begin 0) (end (- (length x) 1))) (sort x begin end)))
再像下面這樣,將 vector-to-list
裸露在外的電線隱藏起來:
(defun vector-to-list (x) (defun to-list (src i dest) (if (< i 0) dest (to-list src (- i 1) (cons (aref src i) dest)))) (let ((x-end (- (length x) 1)) (dest '())) (to-list x x-end dest)))
沒錯,在一個函數的定義裏,能夠定義一個函數。
如今,使用這兩個外殼函數,就能夠將醜陋的
(vector-to-list (sort x 0 end) end '())
殼化(我發明的專業術語)爲
(vector-to-list (vector-sort x))
並且,新的代碼理解起來也很容易,就是對一個向量進行排序,而後再將其轉化爲列表。
同理,可將 list-to-vector
殼化爲:
(defun list-to-vector (x) (defun to-vector (src dest n) (if (null src) dest (progn (aset dest n (car src)) (to-vector (cdr src) dest (+ n 1))))) (let ((dest (make-vector (length x) 0))) (to-vector x dest 0)))
對這三個函數作殼化處理後,那段怪物般的代碼片斷就變得像下面這樣友善可親了,
(vector-to-list (vector-sort (list-to-vector '(3 1 0 8 10 9 5 7 4 999 30))))
寫程序,儘可能先將程序的功能完整且正確地實現出來,而後再考慮如何讓代碼更美觀。這是個人作法。
如今,有個問題,divide
函數也露出了許多電線,要不要也給它作殼化手術呢?我以爲不須要。由於,在邏輯上,它並無暴露在 vector-sort
函數的外部。也就是說,對於要使用 vector-sort
函數對一個向量裏的元素進行排序的時候,divide
不可見。不可見的東西,就不必殼化了。這是個人觀點。
一組數字就像一組撲克牌的牌面。它之因此混亂,是由於周而復始的洗牌,而它們可以得以恢復順序,是由於周而復始的逆洗牌。無他,就是讓一個周而復始的發動機捲去對兩個本身求值,讓這兩個本身分別處理牌面的一個子集。用這種辦法洗牌,就能夠獲得混亂的牌面。用這種辦法排序,就能恢復混亂的牌面。
百川東到海,什麼時候復西歸?到海是洗牌,西歸是排序。這兩個結果應該同時存在,不然河流就會枯竭,海水就會上漲。天然界的水循環系統像是一臺精密的機器,精確地讓河流混亂,又精確地將其復原。
咱們對一組數字進行排序,排序的結果仍是原來的那組數字嗎?
人不可能兩次踏進同一條河流。
下面是本文所述的排序程序的所有代碼,也是我有生以來第一次寫這麼長的 Lisp 代碼。
(defun list-to-vector (x) (defun to-vector (src dest n) (if (null src) dest (progn (aset dest n (car src)) (to-vector (cdr src) dest (+ n 1))))) (let ((dest (make-vector (length x) 0))) (to-vector x dest 0))) (defun divide (x i end location) (if (> i end) location (let* ((pivot (aref x location)) (xi (aref x i)) (next (+ location 1))) (if (> pivot xi) (progn (if (> i next) (aset x i (aref x next))) (aset x location xi) (aset x next pivot) (divide x (+ i 1) end next)) (divide x (+ i 1) end location))))) (defun sort (x begin end) (if (>= begin end) x (let* ((pivot (divide x begin end begin))) (progn (sort x begin pivot) (sort x (+ pivot 1) end))))) (defun vector-sort (x) (let ((begin 0) (end (- (length x) 1))) (sort x begin end))) (defun vector-to-list (x) (defun to-list (src i dest) (if (< i 0) dest (to-list src (- i 1) (cons (aref src i) dest)))) (let ((x-end (- (length x) 1)) (dest '())) (to-list x x-end dest))) (vector-to-list (vector-sort (list-to-vector '(3 1 0 8 10 9 5 7 4 999 30))))
下一篇:咒語