Elisp 04:迭代

前言:不知多久能学会 Elisp

上一章:变量

迭代,亦称循环,表示一段重复运行的程序,其状态可在每次重复运行的过程中发生变化。

基于递归函数可以模拟迭代过程。例如以下程序

(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))))

(progn
  (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 次。`

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 的定义进行求值的过程中。这种优化,叫作尾递归优化。

不过,Elisp 解释器没有尾递归优化的功能,所以它必须提供迭代语法。Elisp 编程时最常用的迭代语法是 while 表达式,用法如下

(while 逻辑表达式
  一段程序)

若逻辑表达式的求值结果为 t,Elisp 便会反复执行 while 表达式里的那段程序,否则,Elisp 解释器会将 nil 作为求值结果,结束对 while 表达式的求值。这意味着 while 表达式的求值结果要么是 nil,要么是 Elisp 解释器永无休止对其进行求值的过程。事实上,while 表达式的求值结果是什么,并不重要,重要的是它的内部如何表达程序运行状态的变化及程序的响应。

基于 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}$ 的高度平衡二叉树几乎不可能具有现实意义。

最后记住一句话吧,迭代是线性的递归。

下一章:文本匹配


5.9k 声望
1.9k 粉丝
0 条评论
5.9k 声望
1.9k 粉丝
宣传栏