Elisp 04:迭代

上一章:變量編程

迭代,亦稱循環,表示一段重複運行的程序,其狀態可在每次重複運行的過程當中發生變化。segmentfault

基於遞歸函數能夠模擬迭代過程。例如如下程序函數

(defun princ\' (x)
  (princ x)
  (princ "\n"))

(defun current-line ()
  (buffer-substring (line-beginning-position) (line-end-position)))

(defun every-line ()
  (if (= (point) (point-max))
      (princ "")
    (progn 
      (princ\' (current-line))
      (forward-line 1)
      (every-line))))

(find-file "foo.md")
(every-line)

Elisp 解釋器在上述程序最後一個表達式 (every-line) 求值時,會轉而對 every-line 函數定義裏的每一個表達式進行求值,可是當 Elisp 解釋器在函數 every-line 的定義裏又遇到了表達式 (every-line),致使它不得再也不次對 every-line 的定義裏的每一個表達式進行求值。該過程周而復始,在每一次反覆對 every-line 的定義進行求值時,princ\' 會不斷輸出當前文本行,而 forward-line 又不斷將插入點移動到下一行的開頭。因而,上述程序便解決了讀取當前緩衝區內的每一行文本並輸出於終端這個問題。優化

咱們活着,也是遞歸吧。如同咱們有壽命同樣,Elisp 對函數的遞歸深度也有限度。every-line 這個函數,最多隻能令 Elisp 解釋器反覆對其求值 max-lisp-eval-depth 次。`code

max-lisp-eval-depth 是 Elisp 解釋器的全局變量,它定義了函數遞歸深度。使用遞歸

(princ\' max-lisp-eval-depth)

可查看它的值,在個人機器上,結果 800,這意味着上述的 every-line 函數只能令 Elisp 解釋器反覆對其求值 800 次。這也意味着,假若當前緩衝區內的文本行數超過 800 行時,every-line 函數的定義會令 Elisp 解釋器因崩潰而終止工做。它的臨終遺言是內存

Lisp nesting exceeds ‘max-lisp-eval-depth’

理論上,假若 Elisp 解釋器可以對相似 every-line 這種尾部遞歸形式的函數予以優化,即可讓本身用無休止地陷入對 every-line 的定義進行求值的過程當中。這種優化,叫做尾遞歸優化。get

不過,Elisp 解釋器沒有尾遞歸優化的功能,因此它必須提供迭代語法。Elisp 編程時最經常使用的迭代語法是 while 表達式,用法以下string

(while 邏輯表達式
  一段程序)

若邏輯表達式的求值結果爲 t,Elisp 便會反覆執行 while 表達式裏的那段程序,不然,Elisp 解釋器會將 nil 做爲求值結果,結束對 while 表達式的求值。這意味着 while 表達式的求值結果要麼是 nil,要麼是 Elisp 解釋器永無休止對其進行求值的過程。事實上,while 表達式的求值結果是什麼,並不重要,重要的是它的內部如何表達程序運行狀態的變化及程序的響應。it

基於 while 語法,可將上述 every-line 函數從新定義爲

(defun every-line ()
  (while (< (point) (point-max))
    (princ\' (current-line))
    (forward-line 1)))

如今,再也不擔憂因當前緩衝區行數過多的狀況了,除非內存不夠用,並且 every-line 的定義也更爲簡潔了。一舉兩得,可是假若我不先用遞歸函數模擬一下迭代過程,就很難有此刻愉悅的心情。

同理,上一章從新定義列表反轉函數

(defun reverse-list (x)
  (let ((y '()))
    (defun reverse-list\' (x)
      (if (null x)
          y
        (progn
          (setq y (cons (car x) y))
          (reverse-list\' (cdr x)))))
    (reverse-list\' x)))

也能夠改寫爲 while 版本:

(defun reverse-list (x)
  (let ((y '()))
    (while (not (null x))
      (setq y (cons (car x) y))
      (setq x (cdr x)))
    y))

其中,not 是 Elisp 的邏輯取反函數。須要注意的是,上述代碼中,局部變量 y 出如今函數定義的最後,它就是函數的求值結果。由於,y 也是 S 表達式,Elisp 可對其進行求值。假若上述函數定義裏的最後一行沒有 y,那麼 while 表達式的求值結果 nil 即是函數的求值結果。

有了迭代,那麼遞歸函數還有必要再使用嗎?

有必要。

一些樹形結構的建立和遍歷,例如二叉樹或多叉樹,用遞歸函數,不只天然,並且代碼也很是簡潔,再者一般也無需擔憂遞歸深度的限制。以高度平衡二叉樹爲例,默認值爲 800 的 max-lisp-eval-depth 足夠了,由於葉結點數量高達 $2^{800}$ 的高度平衡二叉樹幾乎不可能具備現實意義。

最後記住一句話吧,迭代是線性的遞歸。

下一章:文本匹配

相關文章
相關標籤/搜索