Common Lisp宏中LOOP转DO的实现优化及写法合理性问询
关于将Common Lisp宏中LOOP转换为DO的疑问
问题背景
我正在阅读Seibel所著的《Practical Common Lisp》,发现如下示例宏:
(defmacro check (&rest forms) `(progn ,@(loop for f in forms collect `(do-stuff ,f ',f)) (defun test () (check ( (= (+ 1 2 ) 3) (= (+ 1 2 ) 4 ))) )
注:do-stuff仅用于格式化两个参数以测试表达式的真值,与问题无关。
我希望将其中的LOOP转换为DO,已自行实现两种可行方案:
方案一
(defmacro check (&rest forms) `(progn ,@(do ((index 0 (list-length forms)) (accumulator nil)) ((= index (list-length forms)) accumulator) (push `(do-stuff ,(nth index forms) ',(nth index forms)) accumulator) ))
方案二
(defmacro check (&rest forms) `(progn ,@(do* ((index 0 (list-length forms)) (accumulator nil) (f (nth index forms) (nth index forms))) ((= index (list-length forms)) accumulator) (push `(do-stuff ,f ',f) accumulator) ))
我的疑问:
- 是否存在更高效的DO循环写法?
- 当前实现是否合理?
- 能否像LOOP版本那样,无需定义索引变量和累加器列表即可提取列表元素并收集结果?
解答
1. 当前实现的合理性
你的两种方案既存在逻辑错误,又有性能问题:
- 逻辑错误:索引更新表达式写错了——你用
(list-length forms)作为index的增量,这会让index直接跳到列表长度,循环只会执行一次,无法遍历所有forms元素。正确的增量应该是(1+ index)。 - 性能问题:每次循环调用
list-length和nth都会遍历整个列表,时间复杂度从O(n)变成O(n²),元素越多效率越低。
2. 更高效的DO写法
正确且高效的DO写法应该直接遍历列表,避免索引间接访问:
(defmacro check (&rest forms) `(progn ,@(do ((remaining forms (cdr remaining)) (acc nil)) ((null remaining) (reverse acc)) (push `(do-stuff ,(car remaining) ',(car remaining)) acc))))
这个版本的优势:
- 用
remaining跟踪未处理的列表元素,每次取car获取当前元素,cdr推进循环,都是O(1)操作,整体时间复杂度O(n)。 - 仅保留必要的循环变量,逻辑清晰,性能和原LOOP版本一致。
- 最后用
reverse修正push导致的逆序问题,确保生成代码的顺序和输入forms一致。
3. 能否像LOOP那样省略索引和累加器?
DO是显式控制结构,必须声明循环变量和终止条件,无法做到LOOP那种高度抽象的语法。不过上面的优化版本已经去掉了索引变量,仅保留了剩余列表和累加器,这是DO结构下的最简形式。
如果追求更简洁的替代方案,mapcar是更好的选择,语义和原LOOP完全一致,代码更短:
(defmacro check (&rest forms) `(progn ,@(mapcar (lambda (f) `(do-stuff ,f ',f)) forms)))
内容的提问来源于stack exchange,提问作者AVALFINN
相关产品推荐
相关产品推荐

