14.5 Loop 宏 (The Loop Facility)

loop 宏最初是设计来帮助无经验的 Lisp 用户来写出迭代的代码。与其撰写 Lisp 代码,你用一种更接近英语的形式来表达你的程序,然后这个形式被翻译成 Lisp。不幸的是, loop 比原先设计者预期的更接近英语:你可以在简单的情况下使用它,而不需了解它是如何工作的,但想在抽象层面上理解它几乎是不可能的。

如果你是曾经计划某天要理解 loop 怎么工作的许多 Lisp 程序员之一,有一些好消息与坏消息。好消息是你并不孤单:几乎没有人理解它。坏消息是你永远不会理解它,因为 ANSI 标准实际上并没有给出它行为的正式规范。

这个宏唯一的实际定义是它的实现方式,而唯一可以理解它(如果有人可以理解的话)的方法是通过实例。ANSI 标准讨论 loop 的章节大部分由例子组成,而我们将会使用同样的方式来介绍相关的基础概念。

第一个关于 loop 宏我们要注意到的是语法 ( syntax )。一个 loop 表达式不是包含子表达式而是子句 (clauses)。這些子句不是由括号分隔出来;而是每种都有一个不同的语法。在这个方面上, loop 与传统的 Algol-like 语言相似。但其它 loop 独特的特性,使得它与 Algol 不同,也就是在 loop 宏里调换子句的顺序与会发生的事情没有太大的关联。

一个 loop 表达式的求值分为三个阶段,而一个给定的子句可以替多于一个的阶段贡献代码。这些阶段如下:

  1. 序幕 (Prologue)。 被求值一次来做为迭代过程的序幕。包括了将变量设至它们的初始值。
  2. 主体 (Body) 每一次迭代时都会被求值。
  3. 闭幕 (Epilogue) 当迭代结束时被求值。决定了 loop 表达式的返回值(可能返回多个值)。

我们会看几个 loop 子句的例子,并考虑何种代码会贡献至何个阶段。

举例来说,最简单的 loop 表达式,我们可能会看到像是下列的代码:

  1. > (loop for x from 0 to 9
  2. do (princ x))
  3. 0123456789
  4. NIL

这个 loop 表达式印出从 09 的整数,并返回 nil 。第一个子句,

for x from 0 to 9

贡献代码至前两个阶段,导致 x 在序幕中被设为 0 ,在主体开头与 9 来做比较,在主体结尾被递增。第二个子句,

do (princ x)

贡献代码给主体。

一个更通用的 for 子句说明了起始与更新的形式 (initial and update form)。停止迭代可以被像是 whileuntil 子句来控制。

  1. > (loop for x = 8 then (/ x 2)
  2. until (< x 1)
  3. do (princ x))
  4. 8421
  5. NIL

你可以使用 and 来创建复合的 for 子句,同时初始及更新两个变量:

  1. > (loop for x from 1 to 4
  2. and y from 1 to 4
  3. do (princ (list x y)))
  4. (1 1)(2 2)(3 3)(4 4)
  5. NIL

要不然有多重 for 子句时,变量会被循序更新。

另一件在迭代代码通常会做的事是累积某种值。举例来说:

  1. > (loop for x in '(1 2 3 4)
  2. collect (1+ x))
  3. (2 3 4 5)

for 子句使用 in 而不是 from ,导致变量被设为一个列表的后续元素,而不是连续的整数。

在这个情况里, collect 子句贡献代码至三个阶段。在序幕,一個匿名累加器 (anonymous accumulator)設為 nil ;在主体裡, (1+ x) 被累加至這個累加器,而在闭幕时返回累加器的值。

这是返回一个特定值的第一个例子。有用来明确指定返回值的子句,但没有这些子句时,一个 collect 子句决定了返回值。所以我们在这里所做的其实是重复了 mapcar

loop 最常见的用途大概是蒐集调用一个函数数次的结果:

  1. > (loop for x from 1 to 5
  2. collect (random 10))
  3. (3 8 6 5 0)

这里我们获得了一个含五个随机数的列表。这跟我们定义过的 map-int 情况类似 (105 页「译注: 6.4 小节。」)。如果我们有了 loop ,为什么还需要 map-int ?另一个人也可以说,如果我们有了 map-int ,为什么还需要 loop

一个 collect 子句也可以累积值到一个有名字的变量上。下面的函数接受一个数字的列表并返回偶数与奇数列表:

  1. (defun even/odd (ns)
  2. (loop for n in ns
  3. if (evenp n)
  4. collect n into evens
  5. else collect n into odds
  6. finally (return (values evens odds))))

一个 finally 子句贡献代码至闭幕。在这个情况它指定了返回值。

一个 sum 子句和一个 collect 子句类似,但 sum 子句累积一个数字,而不是一个列表。要获得 1n 的和,我们可以写:

  1. (defun sum (n)
  2. (loop for x from 1 to n
  3. sum x))

loop 更进一步的细节在附录 D 讨论,从 325 页开始。举个例子,图 14.1 包含了先前章节的两个迭代函数,而图 14.2 演示了将同样的函数翻译成 loop

  1. (defun most (fn lst)
  2. (if (null lst)
  3. (values nil nil)
  4. (let* ((wins (car lst))
  5. (max (funcall fn wins)))
  6. (dolist (obj (cdr lst))
  7. (let ((score (funcall fn obj)))
  8. (when (> score max)
  9. (setf wins obj
  10. max score))))
  11. (values wins max))))
  12. (defun num-year (n)
  13. (if (< n 0)
  14. (do* ((y (- yzero 1) (- y 1))
  15. (d (- (year-days y)) (- d (year-days y))))
  16. ((<= d n) (values y (- n d))))
  17. (do* ((y yzero (+ y 1))
  18. (prev 0 d)
  19. (d (year-days y) (+ d (year-days y))))
  20. ((> d n) (values y (- n prev))))))

图 14.1 不使用 loop 的迭代函数

  1. (defun most (fn lst)
  2. (if (null lst)
  3. (values nil nil)
  4. (loop with wins = (car lst)
  5. with max = (funcall fn wins)
  6. for obj in (cdr lst)
  7. for score = (funcall fn obj)
  8. when (> score max)
  9. (do (setf wins obj
  10. max score)
  11. finally (return (values wins max))))))
  12. (defun num-year (n)
  13. (if (< n 0)
  14. (loop for y downfrom (- yzero 1)
  15. until (<= d n)
  16. sum (- (year-days y)) into d
  17. finally (return (values (+ y 1) (- n d))))
  18. (loop with prev = 0
  19. for y from yzero
  20. until (> d n)
  21. do (setf prev d)
  22. sum (year-days y) into d
  23. finally (return (values (- y 1)
  24. (- n prev))))))

图 14.2 使用 loop 的迭代函数

一个 loop 的子句可以参照到由另一个子句所设置的变量。举例来说,在 even/odd 的定义里面, finally 子句参照到由两个 collect 子句所创建的变量。这些变量之间的关系,是 loop 定义最含糊不清的地方。考虑下列两个表达式:

  1. (loop for y = 0 then z
  2. for x from 1 to 5
  3. sum 1 into z
  4. finally (return y z))
  5. (loop for x from 1 to 5
  6. for y = 0 then z
  7. sum 1 into z
  8. finally (return y z))

它们看起来够简单 ── 每一个有四个子句。但它们返回同样的值吗?它们返回的值多少?你若试着在标准中想找答案将徒劳无功。每一个 loop 子句本身是够简单的。但它们组合起来的方式是极为复杂的 ── 而最终,甚至标准里也没有明确定义。

由于这类原因,使用 loop 是不推荐的。推荐 loop 的理由,你最多可以说,在像是图 14.2 这般经典的例子中, loop 让代码看起来更容易理解。