Common Lisp 递归宏展开

9
从前,我在玩宏时想到了这个:

(defmacro my-recursive-fact (n)
  (if (= 0 n) '1
    (let ((m (1- n)))
      `(* ,n (my-recursive-fact ,m)))))

它起作用了。

CL-USER> (my-recursive-fact 5)
120

那么我认为,如果我使用macroexpand来展开这个宏,就可以为学生展示递归的一个很好的例子:

CL-USER> (macroexpand '(my-recursive-fact 5))
(* 5 (MY-RECURSIVE-FACT 4))
T

也就是说,在这种情况下,macroexpand-1macroexpand没有区别。我相信我在理解macroexpand方面缺少了一些关键点,而HyperSpec对于递归宏并没有特别说明。

而且我仍然好奇是否有一种方法可以将这种类型的宏扩展到最后。


这似乎不是教授学生宏的恰当示例。您可以在该宏中删除每个撇号、逗号和反引号,将其更改为函数,它将完全评估相同。(实际上,您应该这样做。)我没有任何不尊重的意思,并且我理解对简单示例的渴望,但是展示愚蠢的宏似乎不仅会混淆他们对概念的理解,而且会让他们对整个语言失去兴趣。 - Mr. Lavalamp
4个回答

12

8
MACROEXPAND接受一个表达式并展开它。它会多次执行,直到表达式不再是宏形式。
在你的例子中,对my-recursive-fact的顶层调用是一个宏形式。前面带有乘法的结果表达式不是宏形式,因为*不是宏,它是一个函数。该表达式有一个参数,它是一个宏形式。但是MACROEXPAND不会查看这些参数。
如果你想在所有级别上扩展代码,你需要使用代码遍历器。一些Lisp将其直接集成到IDE中,例如Lispworks。

2
首先,我误解了宏形式的含义。HypepSpec 明确表示它是一个形式,其第一个元素是宏名称。 其次,事实证明我选择的实现 - SBCL - 有自己的代码遍历器。所以我需要的是 sb-walker:walk-form 工具,它给了我想要的输出: (* 5 (* 4 (* 3 (* 2 (* 1 1))))) - alexey.e.egorov

3
你可以使用sb-cltl2:macroexpand-all。最初的回答。
CL-USER> (sb-cltl2:macroexpand-all '(my-recursive-fact 5))
(* 5 (* 4 (* 3 (* 2 (* 1 1)))))

1

编辑:如果在特殊运算符结构的car位置的形式中有任何变量名称与宏相同,那么这将会出现错误。例如:

(let ((setf 10)) (print setf))


这篇文章有些老了,但如果有人想要一种比较通用的方法来递归展开宏,那么请看下文:
(defun recursive-macroexpand (form)
  (let ((expansion (macroexpand form)))
    (if (and (listp expansion) (not (null expansion)))
      (cons (car expansion) (mapcar #'recursive-macroexpand (cdr expansion)))
      expansion)))

例如(在SBCL和CLISP中测试):
(recursive-macroexpand '(my-recursive-fact 5))))

 => (* 5 (* 4 (* 3 (* 2 (* 1 1)))))

一个更丑陋的例子(常规的macroexpand将保留第二个dolist不变):

(recursive-macroexpand
  '(dolist (x '(0 1))
    (dolist (y '(0 1))
      (format t "decimal: ~a binary: ~a~a~%" (+ (* x 2) (* y 1)) x y))))

 => (block nil
     (let* ((#:list-8386 '(0 1)) (x nil)) nil
      (tagbody #:loop-8387 (if (endp #:list-8386) (go #:end-8388)) (setq x (car #:list-8386))
       (block nil
        (let* ((#:list-8389 '(0 1)) (y nil)) nil
         (tagbody #:loop-8390 (if (endp #:list-8389) (go #:end-8391)) (setq y (car #:list-8389)) (format t "decimal: ~a binary: ~a~a~%" (+ (* x 2) (* y 1)) x y)
          (setq #:list-8389 (cdr #:list-8389)) (go #:loop-8390) #:end-8391 (return-from nil (progn nil)))))
       (setq #:list-8386 (cdr #:list-8386)) (go #:loop-8387) #:end-8388 (return-from nil (progn nil)))))

1
如果您将dolist变量之一命名为类似于incf的宏名称,会发生什么? - Dan Robertson
1
@DanRobertson 很好的观点。由于我们在扩展的 result 上进行递归,这对于宏来说不应该是问题,但是期望表单 car 位置上有变量的特殊运算符(如 let)肯定会出现问题。我将编辑我的答案来解决这个问题。 - cebola
1
我写这个的时候所暗示的是,要想做得“正确”,就需要一个代码遍历器。而且,如果你遇到宏定义,事情会更加困难。 - Dan Robertson
@DanRobertson 哦,我现在明白了...是的,那是一个相当幼稚的回答,实际上我经验不太丰富。 - cebola
问题在于,在可移植的Common Lisp中实际上无法回答,因为您无法通过需要&environment的defmacro进行宏展开,因为这样的值无法构造。 - Dan Robertson

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