2.13 迭代 (Iteration)
当我们想重复做一些事情时,迭代比递归来得更自然。典型的例子是用迭代来产生某种表格。这个函数
(defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
列印从 start
到 end
之间的整数的平方:
> (show-squares 2 5)
2 4
3 9
4 16
5 25
DONE
do
宏是 Common Lisp 里最基本的迭代操作符。和 let
类似, do
可以创建变量,而第一个实参是一组变量的规格说明列表。每个元素可以是以下的形式
(variable initial update)
其中 variable 是一个符号, initial 和 update 是表达式。最初每个变量会被赋予 initial 表达式的值;每一次迭代时,会被赋予 update 表达式的值。在 show-squares
函数里, do
只创建了一个变量 i
。第一次迭代时, i
被赋与 start
的值,在接下来的迭代里, i
的值每次增加 1
。
第二个传给 do
的实参可包含一个或多个表达式。第一个表达式用来测试迭代是否结束。在上面的例子中,测试表达式是 (> i end)
。接下来在列表中的表达式会依序被求值,直到迭代结束。而最后一个值会被当作 do
的返回值来返回。所以 show-squares
总是返回 done
。
do
的剩余参数组成了循环的函数体。在每次迭代时,函数体会依序被求值。在每次迭代过程里,变量被更新,检查终止测试条件,接着(若测试失败)求值函数体。
作为对比,以下是递归版本的 show-squares
:
(defun show-squares (i end)
(if (> i end)
'done
(progn
(format t "~A ~A~%" i (* i i))
(show-squares (+ i 1) end))))
唯一的新东西是 progn
。 progn
接受任意数量的表达式,依序求值,并返回最后一个表达式的值。
为了处理某些特殊情况, Common Lisp 有更简单的迭代操作符。举例来说,要遍历列表的元素,你可能会使用 dolist
。以下函数返回列表的长度:
(defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))
这里 dolist
接受这样形式的实参(variable expression),跟着一个具有表达式的函数主体。函数主体会被求值,而变量相继与表达式所返回的列表元素绑定。因此上面的循环说,对于列表 lst
里的每一个 obj
,递增 len
。很显然这个函数的递归版本是:
(defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))
也就是说,如果列表是空表,则长度为 0
;否则长度就是对列表取 cdr
的长度加一。递归版本的 our-length
比较易懂,但由于它不是尾递归(tail-recursive)的形式 (见 13.2 节),效率不是那么高。