2.13 迭代 (Iteration)

当我们想重复做一些事情时,迭代比递归来得更自然。典型的例子是用迭代来产生某种表格。这个函数

  1. (defun show-squares (start end)
  2. (do ((i start (+ i 1)))
  3. ((> i end) 'done)
  4. (format t "~A ~A~%" i (* i i))))

列印从 startend 之间的整数的平方:

  1. > (show-squares 2 5)
  2. 2 4
  3. 3 9
  4. 4 16
  5. 5 25
  6. DONE

do 宏是 Common Lisp 里最基本的迭代操作符。和 let 类似, do 可以创建变量,而第一个实参是一组变量的规格说明列表。每个元素可以是以下的形式

  1. (variable initial update)

其中 variable 是一个符号, initialupdate 是表达式。最初每个变量会被赋予 initial 表达式的值;每一次迭代时,会被赋予 update 表达式的值。在 show-squares 函数里, do 只创建了一个变量 i 。第一次迭代时, i 被赋与 start 的值,在接下来的迭代里, i 的值每次增加 1

第二个传给 do 的实参可包含一个或多个表达式。第一个表达式用来测试迭代是否结束。在上面的例子中,测试表达式是 (> i end) 。接下来在列表中的表达式会依序被求值,直到迭代结束。而最后一个值会被当作 do 的返回值来返回。所以 show-squares 总是返回 done

do 的剩余参数组成了循环的函数体。在每次迭代时,函数体会依序被求值。在每次迭代过程里,变量被更新,检查终止测试条件,接着(若测试失败)求值函数体。

作为对比,以下是递归版本的 show-squares

  1. (defun show-squares (i end)
  2. (if (> i end)
  3. 'done
  4. (progn
  5. (format t "~A ~A~%" i (* i i))
  6. (show-squares (+ i 1) end))))

唯一的新东西是 prognprogn 接受任意数量的表达式,依序求值,并返回最后一个表达式的值。

为了处理某些特殊情况, Common Lisp 有更简单的迭代操作符。举例来说,要遍历列表的元素,你可能会使用 dolist 。以下函数返回列表的长度:

  1. (defun our-length (lst)
  2. (let ((len 0))
  3. (dolist (obj lst)
  4. (setf len (+ len 1)))
  5. len))

这里 dolist 接受这样形式的实参(variable expression),跟着一个具有表达式的函数主体。函数主体会被求值,而变量相继与表达式所返回的列表元素绑定。因此上面的循环说,对于列表 lst 里的每一个 obj ,递增 len 。很显然这个函数的递归版本是:

  1. (defun our-length (lst)
  2. (if (null lst)
  3. 0
  4. (+ (our-length (cdr lst)) 1)))

也就是说,如果列表是空表,则长度为 0 ;否则长度就是对列表取 cdr 的长度加一。递归版本的 our-length 比较易懂,但由于它不是尾递归(tail-recursive)的形式 (见 13.2 节),效率不是那么高。