如果你是曾经计划某天要理解 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 表达式,我们可能会看到像是下列的代码:

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

    for x from 0 to 9

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

    贡献代码给主体。

    一个更通用的 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 子句时,变量会被循序更新。

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

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

    在这个情况里, 子句贡献代码至三个阶段。在序幕,一個匿名累加器 (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)

    一个 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 的和,我们可以写:

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

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

    图 14.1 不使用 loop 的迭代函数

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

    图 14.2 使用 loop 的迭代函数

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

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

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