Lisp宏(或函数)用于嵌套循环

7

是否可以编写一个通用Lisp宏,它接受维度和变量列表、迭代的主体,并创建由指定数量的嵌套循环组成的代码?

也就是说,类似于:

(nested-loops '(2 5 3) '(i j k) whatever_loop_body)

应该扩展为
(loop for i from 0 below 2 do
  (loop for j from 0 below 5 do
    (loop for k from 0 below 3 do
      whatever_loop_body)))

跟进

正如huaiyuan所指出的那样,我必须在编译时知道传递给宏的参数。如果您确实需要像我一样的函数,请看下面。

如果您可以使用宏,请尝试6502的递归解决方案,非常棒。


你使用哪种Lisp方言?是Common Lisp吗? - seh
3个回答

8

您不需要使用引号,因为在编译时必须知道尺寸和变量。

(defmacro nested-loops (dimensions variables &body body)
  (loop for range in (reverse dimensions)
        for index in (reverse variables)
        for x = body then (list y)
        for y = `(loop for ,index from 0 to ,range do ,@x)
        finally (return y)))

编辑:

如果在编译时无法确定尺寸,我们将需要一个函数。

(defun nested-map (fn dimensions)
  (labels ((gn (args dimensions)
             (if dimensions
               (loop for i from 0 to (car dimensions) do
                 (gn (cons i args) (cdr dimensions)))
               (apply fn (reverse args)))))
    (gn nil dimensions)))

在调用时,需要使用lambda将内容进行包装。

CL-USER> (nested-map (lambda (&rest indexes) (print indexes)) '(2 3 4))

(0 0 0) 
(0 0 1) 
(0 0 2) 
(0 0 3) 
(0 0 4) 
(0 1 0) 
(0 1 1) 
(0 1 2) 
(0 1 3) 
(0 1 4) 
(0 2 0) 
(0 2 1) 
...

编辑(2012-04-16):

上面的嵌套映射版本是为了更接近原始问题陈述而编写的。正如mmj在评论中所说,将索引范围从0到n-1更自然,并且如果我们不坚持按行主顺序迭代,则将反转移出内部循环应该提高效率。此外,更明智的做法可能是使输入函数接受元组而不是单个索引,以便与级别无关。以下是带有上述更改的新版本:

(defun nested-map (fn dimensions)
  (labels ((gn (args dimensions)
             (if dimensions
               (loop for i below (car dimensions) do
                 (gn (cons i args) (cdr dimensions)))
               (funcall fn args))))
    (gn nil (reverse dimensions))))

那么,

CL-USER> (nested-map #'print '(2 3 4))

函数也很棒。我喜欢你的编码。由于我更喜欢从左侧开始滚动索引,因此我需要用'args'替换'(reverse args)',并在最后一行将'dimensions'替换为'(reverse dimensions)'。 - mmj
如果n是一个维度的值,我建议循环(在函数中)应该从0到n-1(我的选择),或者从1到n。在前一种情况下,您只需要在循环子句中用“below”替换“to”。 - mmj

7
有时候一种有用的方法是编写递归宏,即编写一个生成代码的宏,除非情况足够简单可以直接解决,否则该宏将包含另一个调用相同宏的代码:
(defmacro nested-loops (max-values vars &rest body)
  (if vars
      `(loop for ,(first vars) from 0 to ,(first max-values) do
          (nested-loops ,(rest max-values) ,(rest vars) ,@body))
      `(progn ,@body)))

(nested-loops (2 3 4) (i j k)
  (print (list i j k)))

如果变量列表为空,则宏直接扩展为主体形式,否则生成的代码是第一个变量上的(loop...),其中包含另一个(nested-loops ...)在do部分进行调用。

该宏在函数中通常使用递归的意义上不是递归(它没有直接调用自己),但是宏扩展逻辑将对内部部分调用相同的宏,直到完成代码生成。

请注意,在内部循环中使用的最大值表达式将在外部循环的每次迭代中重新评估。如果这些表达式确实像您的测试案例中那样是数字,则不会有任何区别,但是如果它们例如是函数调用,则情况就不同了。


只是一个样式建议; 尝试在变量和计数为空时设置基本情况。然后,宏级逻辑将首先出现,并根据基本情况生成不同的代码。然后,dotimes 代码仅在非基本情况分支中,body 仅在基本情况分支中,您应该少做一些反引号杂技。@mmj《让超越Lambda》有一章关于编写递归宏的好文章。 - Clayton Stanley
@mmj:我不习惯使用“循环”,所以我没有意识到你正在寻找特定的结构。通常我会从“0”循环到“(1-n)”(因此有“n”次迭代)。这两个问题都已经解决,现在宏与你的问题文本匹配(说实话,我最初是在Lisp方言REPL中实现它的,那里没有循环设施)。 - 6502
我注意到在我的代码中反复出现了(progn ,@body)的模式;我从来没有找到一个简洁的方法来避免这个习语,也没有任何实际理由考虑不使用它。 - Clayton Stanley
@6502 - 原问题中的迭代限制有误,是我的错:现在我已经修正了它们,选择了0/n-1。关于要生成的结构,如果您可以使用'do'或'dotimes'语句达到相同的目标,而不需要宏展开为“loop”语句,那就非常好了,甚至更好。 - mmj
@6502- 在你的编辑后,代码变得清晰明了,优美非凡。这个递归解决方案正是我在发布问题之前想到的。我真的想接受这两个答案。 - mmj
显示剩余3条评论

2

嗯。这是common lisp中这样一个宏的示例。不过,需要注意的是,我并不确定这是否真的是个好主意。但我们都是成年人,不是吗?

(defmacro nested-loop (control &body body)
  (let ((variables ())
        (lower-bounds ())
        (upper-bounds ()))
    (loop
       :for ctl :in (reverse control)
       :do (destructuring-bind (variable bound1 &optional (bound2 nil got-bound2)) ctl
             (push variable variables)
             (push (if got-bound2 bound1 0) lower-bounds)
             (push (if got-bound2 bound2 bound1) upper-bounds)))
    (labels ((recurr (vars lowers uppers)
               (if (null vars)
                   `(progn ,@body)
                   `(loop 
                       :for ,(car vars) :upfrom ,(car lowers) :to ,(car uppers)
                       :do ,(recurr (cdr vars) (cdr lowers) (cdr uppers))))))
      (recurr variables lower-bounds upper-bounds))))

语法与您的提议略有不同。
(nested-loop ((i 0 10) (j 15) (k 15 20)) 
    (format t "~D ~D ~D~%" i j k))

扩展为

(loop :for i :upfrom 0 :to 10
  :do (loop :for j :upfrom 0 :to 15
            :do (loop :for k :upfrom 15 :to 20
                      :do (progn (format t "~d ~d ~d~%" i j k)))))

该宏的第一个参数是一个列表,其形式为列表的列表。
(variable upper-bound)

(暗含下限为0)或
(variable lower-bound upper-bounds)

如果再加上一些细节处理,就可以得到类似于以下的效果:

(nested-loop ((i :upfrom 10 :below 20) (j :downfrom 100 :to 1)) ...)

但是,如果loop已经拥有所有这些功能,为什么还要费心呢?

我选择了另一个答案,因为它更简洁,但我承认你的更灵活。我印象深刻。 - mmj

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