5.4 迭代 (Iteration)
最基本的迭代操作符是 do
,在 2.13 小节介绍过。由于 do
包含了隐式的 block
及 tagbody
,我们现在知道是可以在 do
主体内使用 return
、 return-from
以及 go
。
2.13 节提到 do
的第一个参数必须是说明变量规格的列表,列表可以是如下形式:
(variable initial update)
initial
与 update
形式是选择性的。若 update
形式忽略时,每次迭代时不会更新变量。若 initial
形式也忽略时,变量会使用 nil
来初始化。
在 23 页的例子中(译注: 2.13 节),
(defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
update
形式引用到由 do
所创造的变量。一般都是这么用。如果一个 do
的 update
形式,没有至少引用到一个 do
创建的变量时,反而很奇怪。
当同时更新超过一个变量时,问题来了,如果一个 update
形式,引用到一个拥有自己的 update
形式的变量时,它会被更新呢?或是获得前一次迭代的值?使用 do
的话,它获得后者的值:
> (let ((x 'a))
(do ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y)))
(1 A) (2 1) (3 2) (4 3) (5 4)
NIL
每一次迭代时, x
获得先前的值,加上一; y
也获得 x
的前一次数值。
但也有一个 do*
,它有着和 let
与 let*
一样的关系。任何 initial
或 update
形式可以参照到前一个子句的变量,并会获得当下的值:
> (do* ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y))
(1 1) (2 2) (3 3) (4 4) (5 5)
NIL
除了 do
与 do*
之外,也有几个特别用途的迭代操作符。要迭代一个列表的元素,我们可以使用 dolist
:
> (dolist (x '(a b c d) 'done)
(format t "~A " x))
A B C D
DONE
当迭代结束时,初始列表内的第三个表达式 (译注: done
) ,会被求值并作为 dolist
的返回值。缺省是 nil
。
有着同样的精神的是 dotimes
,给定某个 n
,将会从整数 0
,迭代至 n-1
:
(dotimes (x 5 x)
(format t "~A " x))
0 1 2 3 4
5
dolist
与 dotimes 初始列表的第三个表达式皆可省略,省略时为 ``nil
。注意该表达式可引用到迭代过程中的变量。
(译注:第三个表达式即上例之 x
,可以省略,省略时 dotimes
表达式的返回值为 nil
。)
Note
do 的重点 (THE POINT OF do)
在 “The Evolution of Lisp” 里,Steele 与 Garbriel 陈述了 do 的重点, 表达的实在太好了,值得整个在这里引用过来:
撇开争论语法不谈,有件事要说明的是,在任何一个编程语言中,一个循环若一次只能更新一个变量是毫无用处的。 几乎在任何情况下,会有一个变量用来产生下个值,而另一个变量用来累积结果。如果循环语法只能产生变量, 那么累积结果就得借由赋值语句来“手动”实现…或有其他的副作用。具有多变量的 do 循环,体现了产生与累积的本质对称性,允许可以无副作用地表达迭代过程:
(defun factorial (n)
(do ((j n (- j 1))
(f 1 (* j f)))
((= j 0) f)))
当然在 step 形式里实现所有的实际工作,一个没有主体的 do 循环形式是较不寻常的。
函数 mapc
和 mapcar
很像,但不会 cons
一个新列表作为返回值,所以使用的唯一理由是为了副作用。它们比 dolist
来得灵活,因为可以同时遍历多个列表:
> (mapc #'(lambda (x y)
(format t "~A ~A " x y))
'(hip flip slip)
'(hop flop slop))
HIP HOP FLIP FLOP SLIP SLOP
(HIP FLIP SLIP)
总是返回 mapc
的第二个参数。