【问题标题】:Lisp: can a macro be recursive?Lisp:宏可以递归吗?
【发布时间】:2011-12-28 05:50:03
【问题描述】:

我最近开始使用 Lisp 进行编码,并且已经对宏印象深刻 - 它们在编译时 allowed me to do complex loop-unrolling,这是我无法用我所知道的任何其他语言优雅地做到这一点的(即代码-在保持原始结构的同时生​​成)。

关于优化:我在同一个代码中添加了类型注释(很多“fixnum”)。一旦我添加了 3 或 4 个,我意识到我做错了 - 这就是宏的用途,不要重复你自己......

; whenever we want to indicate that the result of an operation 
; fits in a fixnum, we macro expand (the fixnum (...))
(defmacro fast (&rest args)
  `(the fixnum ,args))
...
(cond
  (...)
  (t (let* ((forOrange (+ (aref counts 5)
                          (fast * 2 (aref counts 6))
                          (fast * 5 (aref counts 7))
                          (fast * 10 (aref counts 8))))
            (forYellow (+ (aref counts 3)
                          (fast * 2 (aref counts 2))
                          (fast * 5 (aref counts 1))
                          (fast * 10 (aref counts 0))))

...确实,这行得通:我没有在各处写很多“(the fixnum (...))”,而是快速在表达式前面加上“fast” - 一切都很好。

然后……

我意识到即使这不是事情应该停止的地方:原则上,宏“快速”应该......在评估的顶部调用,在这种情况下:

            (forYellow (fast + (aref counts 3)
                          (* 2 (aref counts 2))
                          (* 5 (aref counts 1))
                          (* 10 (aref counts 0))))

...并且它应该在所有子表达式中递归地“种植”“(the fixnum (...))”。

这可以吗? “defmacro”可以递归吗?

更新:我在尝试执行此操作时遇到了一些非常奇怪的问题,因此我最终按照 Rord 的建议执行了以下操作 - 即实现了一个函数,在 repl 中对其进行了测试,然后从宏中调用它:

(defun operation-p (x)
  (or (equal x '+) (equal x '-) (equal x '*) (equal x '/)))

(defun clone (sexpr)
  (cond
    ((listp sexpr)
     (if (null sexpr)
       ()
       (let ((hd (car sexpr))
             (tl (cdr sexpr)))
         (cond
           ((listp hd) (append (list (clone hd)) (clone tl)))
           ((operation-p hd) (list 'the 'fixnum (cons hd (clone tl))))
           (t (cons hd (clone tl)))))))
    (t sexpr)))

(defmacro fast (&rest sexpr)
  `(,@(clone sexpr)))

它在 SBCL 下工作正常:

$ sbcl
This is SBCL 1.0.52, an implementation of ANSI Common Lisp.
...
* (load "score4.cl")

T
* (setf a '(+ (1 2) (- 1 (+ 5 6)))
...
* (clone a)

(THE FIXNUM (+ (1 2) (THE FIXNUM (- 1 (THE FIXNUM (+ 5 6))))))

* (macroexpand '(fast + 1 2 THE FIXNUM (- 1 THE FIXNUM (+ 5 6))))

(THE FIXNUM (+ 1 2 THE FIXNUM (THE FIXNUM (- 1 THE FIXNUM (THE FIXNUM (+ 5 6))))))
T

一切都很好,除了一个副作用:CMUCL 工作,但不再编译代码:

; Error: (during macroexpansion)
; Error in KERNEL:%COERCE-TO-FUNCTION:  the function CLONE is undefined.

哦,好吧:-)

更新:编译失败在a different SO question中得到解决。

【问题讨论】:

  • 一个风格建议:不要将要转换的表格拼接成宏调用。将其保留为子表单,即更喜欢 (fast (* ...)) 而不是 (fast * ...)。它使语义更简单(考虑(fast 3) 的情况——这个表单应该是什么意思?)并且迎合了读者对表单结构的期望。
  • @Matthias:最初我是这样的——但我意识到在这种情况下存在明确的语义(即“(fast 3)”只能表示“3”)并且......它更易于使用:您会在代码中看到要强制转换为 fixnum 的位置,您不必寻找paren-matching:只需在前面输入“fast”即可。
  • 这在Scheme中很常见。
  • @leppie 对。它甚至有内置的先例,例如map:(map bling xs)。但请注意,这不适用于 CL,您必须在其中编写 (mapcar #'bling xs)。产生差异的原因是 Scheme 是一个 Lisp-1,所以对于 bling 的含义没有任何混淆。在 CL 中,当我看到 bling 处于非操作员位置时,我认为它是 变量 bling。因此,我对我认为与读者期望相矛盾的内容提出警告。
  • @ttsiodras 在调用中包含表单时必须寻找括号意味着您没有使用足够好的编辑器。 ;) 使用 paredit,尝试C-1 (

标签: macros lisp common-lisp


【解决方案1】:

宏不仅被调用,而且在使用时会扩展,因此在自己的定义中引用宏会变得混乱。但是您不必这样做:宏可以调用常规函数,因此您可以编写一个常规函数来进行递归列表处理,然后只需从宏中使用它即可。

【讨论】:

  • 我按照你的建议做了(用我的解决方案更新了上面的问题)。我现在的问题是,即使它运行,也无法使用 CMUCL 编译此代码:(compile-file "bug.cl") 失败...为此打开新的 SO 问题。
  • 实际上。在自己的定义中引用宏是完全可以的。其实cond就是这样实现的
  • @erjoalgo:我并不是说它不能工作,但它比常规递归函数更难推理。
  • 实际上,在许多情况下,使用常规扩展结构来推理宏是最自然的方法,就像@Kaz 在他的回答中指出的那样
【解决方案2】:

您绝对可以编写一个在其自身的扩展中使用自身的宏。 这很有意义,而且这是编写 COND 宏的自然方式,它具有规则的扩展结构,因为任意长的 cond 对列表,可以通过通常的 car/cdr 或 first/rest 递归来表示。

(defmacro new-cond (&rest cond-pairs)
  (cond      ;; you need cond to compile new-cond syntax, LOL!
    ((null cond-pairs) nil)
    ((atom cond-pairs) (error "new-cond: bad syntax!"))
    (t `(if ,(first (first cond-pairs))
           (progn ,@(rest (first cond-pairs)))
           (new-cond ,@(rest cond-pairs))))))


> (macroexpand-1 '(new-cond (1 2) (3 4)))

(IF 1 (PROGN 2) (NEW-COND (3 4)))

这非常类似于惰性列表处理。 macroexpand 仅扩展外部宏,留下一个“承诺”对象继续扩展。那个“promise”对象就是尾部的宏调用(NEW-COND (3 4))

当然,真正的宏扩展器将遍历整个表单并“强制”所有这些承诺(未扩展的宏调用),直到不再存在。

我认为这种风格有一些优点,例如使用macroexpand 轻松调试。你得到一个小的扩展。如果它的递归性质很明显,那就是胜利。如果宏是这样的,你的大脑需要看到整个事物的扩展,那是一种损失。

在这里,我可以看到(IF 1 (PROGN 2) (NEW-COND (3 4))) 是正确的编译。如果 1 为真,则评估表单列表 (2)。否则继续使用其他条件对。现在我们必须验证一对情况:

> (macroexpand-1 '(new-cond (3 4)))
(IF 3 (PROGN 4) (NEW-COND))

非常好,无对的情况(NEW-COND),通过明显的检查,减少到零。

其次,宏也可以在它自己的代码中调用它自己。因此,例如,如果我们是试图定义 cond 的 Lisp 实现者,我们有一种方法可以真正摆脱困境:

(defmacro cond (&rest cond-pairs)
  (cond      ;; you need cond to compile new-cond syntax, LOL!
    ((null cond-pairs) nil)
    ((atom cond-pairs) (error "new-cond: bad syntax!"))
    (t `(if ,(first (first cond-pairs))
           (progn ,@(rest (first cond-pairs)))
           (new-cond ,@(rest cond-pairs))))))

cond 如何在定义它的宏中使用?答案是自举。在宏中展开的cond 是我们已经拥有的现有cond。这个新定义可以很好地扩展现有的cond,然后替换它。

这个过程可以重复。我们可以一次又一次地评估上面的defmacro表格;上一次评估刚刚安装的cond 用于扩展新评估中出现的cond

如果我们用现有的一个 Lisp 编译器来提升一个 Lisp 编译器,我们可以使用宿主 Lisp 的 cond 启动这个过程,所以我们不需要第二个提升版本的 cond 宏依赖cond

请注意,要使其正常工作,我们的替换 cond 宏的主体必须在安装为 cond 的新定义之前完全展开。如果宏定义的主体未展开,这将不起作用;然后它变成一个递归调用。换句话说,这不是递归。递归不起作用;宏不能需要自身来扩展自身。充其量它可能需要一个不是自身但恰好具有相同名称的现有宏。

【讨论】:

  • 如果你使用嵌套的 if,你可能会在 new-cond 中丢失 cond。 (if (null cond-pairs) nil (if (atom cond-pairs) (error "new-cond: bad syntax!") '(if ,(first ...(如果应该是`之前)
  • @jcubic 如果我们实际上定义了cond,而只有if,那确实是必要的。
【解决方案3】:

当然。您可以编写一个宏,递归地遍历参数形式并按照您想要的方式转换它们,一次一个子形式。由于这比听起来要复杂一些,因此正确的做法是使用库进行代码遍历

Quicklisp 包括hu.dwim.walker,这显然是arnesi 代码漫游器的改进版本。

【讨论】:

    猜你喜欢
    • 2013-05-03
    • 2023-03-13
    • 2013-12-09
    • 1970-01-01
    • 1970-01-01
    • 2011-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多