在Common Lisp宏中扩展参数列表

4

我正在尝试学习常见的Lisp语言,并且作为宏编写的练习,我正在尝试创建一个定义任意深度嵌套do循环的宏。我在使用emacs和slime, 以sbcl为基础进行编写。

首先,我编写了这个双重循环宏:

(defmacro nested-do-2 (ii jj start end &body body)
  `(do ((,ii ,start (1+ ,ii)))
       ((> ,ii ,end))
     (do ((,jj ,ii (1+ ,jj)))
         ((> ,jj ,end))
       ,@body)))

我可以将其用作以下方式:
(nested-do-2 ii jj 10 20 (print (+ ii jj)))

顺便说一下,我最初使用gensym生成循环计数器(ii,jj)编写了这个宏,但后来我意识到如果不能在主体中访问计数器,那么这个宏就没什么用了。

无论如何,我想将该宏概括为一个嵌套-do循环,可以嵌套到任意级别。这是我目前的成果,但它并不完全有效:

(defmacro nested-do ((&rest indices) start end &body body)
  `(dolist ((index ,indices))
     (do ((index ,start (1+ index)))
          ((> index ,end))
        (if (eql index (elt ,indices (elt (reverse ,indices) 0)))
            ,@body))))

我希望您能按照以下方式调用它:
(nested-do (ii jj kk) 10 15 (print (+ ii jj kk)))

然而,列表未能正确扩展,我最终在调试器中遇到了这个错误:
error while parsing arguments to DEFMACRO DOLIST:                                              
  invalid number of elements in                                                                
    ((INDEX (II JJ KK)))

如果不明显,嵌套的 if 语句的目的是仅在最内层循环中执行主体。这对我来说并不是非常优雅,而且它没有真正测试过(因为我还不能扩展参数列表),但这并不是这个问题的重点。
如何在宏中正确地扩展列表?问题出在宏语法还是函数调用中列表的表达式上?任何其他评论也将不胜感激。
提前致谢。

为什么不将计数器列表视为单个参数?(defmacro nested-do indices start end &body body),并且让(indices = '(ii jj kk))。 - ApproachingDarknessFish
1
请查看这个问题 - huaiyuan
@ValekHalfHeart - 那样也可以,但我认为使用(&rest indices)会使代码更加自说明,而不仅仅是indices。当我进行了那个更改后,我仍然无法按照我预期的方式展开indices。 - Ampers4nd
@huaiyuan - 感谢提供链接。那里有很多有趣的回答。你的答案简洁明了,我也喜欢6502的递归宏展开。 - Ampers4nd
2个回答

1

以下是一种方法 - 从底部(循环体)向上构建每个索引的结构:

(defmacro nested-do ((&rest indices) start end &body body)
  (let ((rez `(progn ,@body)))
    (dolist (index (reverse indices) rez)
      (setf rez
            `(do ((,index ,start (1+ ,index)))
                 ((> ,index ,end))
               ,rez)))))

内部索引应从前一个索引的相应当前值开始运行。 - Svante
这个很好用。谢谢。不过,我仍然很好奇为什么我上面发布的代码没有扩展。我想我在引用方面可能漏掉了一些东西,因为我还缺乏经验。你有什么想法吗? - Ampers4nd
2
这行代码有两个问题:(dolist ((index ,indices)) - dolist 不需要双括号 (((),而且 ,indices 展开为 (ii jj kk),但它们应该展开为 (list ii jj kk),否则它们会被视为将 ii 作为函数应用于参数 jjkk。因此,该行代码应该改为:(dolist (index (list ,@indices)) - Vsevolod Dyomkin

0

[除了负面评价,这个实际上是有效的,而且还很漂亮!]

为了清晰地说明宏定义的递归性质,这里提供一个Scheme实现:

(define-syntax nested-do
  (syntax-rules ()
    ((_ ((index start end)) body)
     (do ((index start (+ 1 index)))
         ((= index end))
       body))

    ((_ ((index start end) rest ...) body)
     (do ((index start (+ 1 index)))
         ((= index end))
       (nested-do (rest ...) body)))))

使用上述模板,类似以下代码即可完成:

(defmacro nested-do ((&rest indices) start end &body body)
  (let ((index (car indices)))
    `(do ((,index ,start (1+ ,index)))
         ((> ,index ,end))
       ,(if (null (cdr indices))
            `(progn ,@body)
            `(nested-do (,@(cdr indices)) ,start ,end ,@body)))))


* (nested-do (i j) 0 2 (print (list i j)))
(0 0) 
(0 1) 
(0 2) 
(1 0) 
(1 1) 
(1 2) 
(2 0) 
(2 1) 
(2 2) 
NIL

请注意,使用所有Common-Lisp宏时,您需要使用“gensym”模式以避免变量捕获。

递归宏加1。请注意,此宏不需要gensyms,因为它不会向环境中引入新符号。 - Clayton Stanley
引入本地变量 'index'。 - GoZoner
“index”没有被引入。你的示例展开为(DO ((I 0 (1+ I))) ((> I 2)) (DO ((J 0 (1+ J))) ((> J 2)) (PROGN (PRINT (LIST I J)))))。其中没有“index”。 - lmj
还有一个关于风格的小问题:我建议写成,(foo)而不是(,@(foo)) - lmj
1
你和@clayton都是对的,没有引入任何符号。虽然(foo,)可能有效,但我使用了(,@(car indices)),以便外部括号可以轻松地被视为(nested-do (...) ...)的语法元素。 - GoZoner
就此而言,我同意GoZoner在这种情况下使用(@(foo))的风格。但我们只是纠结于细节 :) - Clayton Stanley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接