扩展到同一运算符的宏

Macro expanding to same operator

许多 Lisp 家族语言都有一些语法糖,例如允许两个以上操作数的加法或比较,if 可选地省略备用分支等。实现时会有一些话要说这些带有宏,可以将 (+ a b c) 扩展为 (+ a (+ b c)) 等;这将使实际的 运行 时间代码更清晰、更简单且速度稍快(因为每次添加一对数字时检查额外参数的逻辑不必 运行)。

不过,通常的宏展开算法是'keep expanding the outermost form over and over until you get a non-macro result'。所以这意味着例如+ 最好不要是扩展为 + 的宏,即使是缩减版本,否则你会陷入无限循环。

有没有现成的 Lisp 可以解决宏扩展时的这个问题?如果是,它是如何做到的?

Common Lisp 提供 compiler macros.

这些可以用于此类优化。编译器宏可以有条件地 decline 通过仅返回表单本身来提供扩展。

这是 Rainer 回答的附录:这个回答实际上只是给出了一些例子。

首先,编译诸如算术运算之类的东西是一件棘手的事情,因为你有一种特别的动机去尝试尽可能多地将其转化为机器可以理解的操作,如果不这样做,可能会导致数字密集型应用程序的速度大幅下降代码。所以通常编译器有很多关于如何编译东西的知识,也有很多自由:例如在 CL (+ a 2 b 3) 中,编译器可以将其转换为 (+ 5 a b):允许编译器重新排序和合并事物(但不能更改求值顺序:它可以将 (+ (f a) (g b)) 变成类似 (let ((ta (f a)) (tb (g b))) (+ tb ta)) 但不能变成 (+ (g b) (f a)) 的东西)。

所以算术通常很神奇。但是看看如何使用宏来做到这一点以及为什么在 CL 中需要编译器宏仍然很有趣。

(注:下面所有的宏都是我没多想就写的:它们可能在语义上是错误的。)

宏:错误答案

所以,加法,在 CL 中。一个明显的技巧是拥有一个 'primitive-two-arg' 函数(在好的情况下,编译器可能会内联到汇编中),然后让 public 接口成为一个扩展为该接口的宏。

所以,这是

(defun plus/2 (a b)
  ;; just link to the underlying CL arithmetic
  (+ a b))

然后您可以用显而易见的方式编写通用函数:

(defun plus/many (a &rest bcd)
  (if (null bcd)
      a
    (reduce #'plus/2 bcd :initial-value a)))

现在您可以编写 public 接口,plus 作为宏:

(defmacro plus (a &rest bcd)
  (cond ((null bcd)
         a)
        ((null (rest bcd))
         `(plus/2 ,a ,(first bcd)))
        (t
         `(plus/2 (plus/2 ,a ,(first bcd))
                  (plus ,@(rest bcd))))))

你可以看到

  • (plus a b) 扩展为 (plus/2 a b)'
  • (plus a b c) 扩展为 (plus/2 (plus/2 a b) (plus c)),然后扩展为 (plus/2 (plus/2 a b) c)

我们可以做得更好:

(defmacro plus (a &rest bcd)
  (multiple-value-bind (numbers others) (loop for thing in (cons a bcd)
                                             if (numberp thing)
                                             collect thing into numbers
                                             else collect thing into things
                                             finally (return (values numbers things)))
    (cond ((null others)
           (reduce #'plus/2 numbers :initial-value 0))
          ((null (rest others))
           `(plus/2 ,(reduce #'plus/2 numbers :initial-value 0)
                    ,(first others)))
          (t
           `(plus/2 ,(reduce #'plus/2 numbers :initial-value 0)
                    ,(reduce (lambda (x y)
                               `(plus/2 ,x ,y))
                             others))))))

现在您可以将 (plus 1 x y 2.0 3 z 4 a) 扩展为 (plus/2 10.0 (plus/2 (plus/2 (plus/2 x y) z) a)),我认为这对我来说还不错。

但这是无望的。这是无望的,因为如果我说 (apply #'plus ...) 会发生什么? Doom: plus 必须是函数,不能是宏。

编译器宏:正确答案

这就是编译器宏发挥作用的地方。让我们重新开始,但这次函数(上面从未使用过)plus/many 将只是 plus:

(defun plus/2 (a b)
  ;; just link to the underlying CL arithmetic
  (+ a b))

(defun plus (a &rest bcd)
  (if (null bcd)
      a
    (reduce #'plus/2 bcd :initial-value a)))

现在我们可以为plus写一个编译器宏,它是编译器可能使用的特殊宏:

The presence of a compiler macro definition for a function or macro indicates that it is desirable for the compiler to use the expansion of the compiler macro instead of the original function form or macro form. However, no language processor (compiler, evaluator, or other code walker) is ever required to actually invoke compiler macro functions, or to make use of the resulting expansion if it does invoke a compiler macro function. – CLHS 3.2.2.1.3

(define-compiler-macro plus (a &rest bcd)
  (multiple-value-bind (numbers others) (loop for thing in (cons a bcd)
                                             if (numberp thing)
                                             collect thing into numbers
                                             else collect thing into things
                                             finally (return (values numbers things)))
    (cond ((null others)
           (reduce #'plus/2 numbers :initial-value 0))
          ((null (rest others))
           `(plus/2 ,(reduce #'plus/2 numbers :initial-value 0)
                    ,(first others)))
          (t
           `(plus/2 ,(reduce #'plus/2 numbers :initial-value 0)
                    ,(reduce (lambda (x y)
                               `(plus/2 ,x ,y))
                             others))))))

请注意,此编译器宏的主体 与上面作为宏的 plus 的第二个定义相同:它是相同的,因为此函数没有任何情况宏要下降扩展的地方。

你可以用compiler-macroexpand检查扩展:

> (compiler-macroexpand '(plus 1 2 3 x 4 y 5.0 z))
(plus/2 15.0 (plus/2 (plus/2 x y) z))
t

第二个值表示编译器宏没有拒绝扩展。并且

> (apply #'plus '(1 2 3))
6

看起来不错。

与普通宏不同,像这样的宏 可以 拒绝扩展,它通过返回整个宏形式不变来实现。例如,这是上面宏的一个版本,它只处理非常简单的情况:

(define-compiler-macro plus (&whole form a &rest bcd)
  (cond ((null bcd)
         a)
        ((null (rest bcd))
         `(plus/2 ,a ,(first bcd)))
        (t                              ;cop out
         form)))

现在

 > (compiler-macroexpand '(plus 1 2 3 x 4 y 5.0 z))
(plus 1 2 3 x 4 y 5.0 z)
nil

但是

> (compiler-macroexpand '(plus 1 2))
(plus/2 1 2)
t

好的。