Elisp编译宏展开之谜

Compile expansion mystery of macro in Elisp

我总是对 Emacs 中的 macro 感到困惑。关于如何使用macro的文档已经有很多了。但是文档只提到了表面,例子往往过于简单。另外,搜索 macro 本身是相当困难的。它将提供键盘宏结果。

人们总是说宏在编译时展开,它和直接编写相同的代码副本一样快。这总是正确的吗?

我从例子开始when

(pp-macroexpand-expression
 '(when t
    (print "t")))

;; (if t
;;     (progn
;;       (print "t")))

当我们在编译时使用when时,(if t .....)直接插入到我们的代码中,对吗?

然后,我从subr.el中找到一个更复杂的例子dotimes。我稍微简化了代码:

(defmacro dotimes (spec &rest body)

  (declare (indent 1) (debug dolist))

  (let ((temp '--dotimes-limit--)
        (start 0)
        (end (nth 1 spec)))

    `(let ((,temp ,end)
           (,(car spec) ,start))
       (while (< ,(car spec) ,temp)
         ,@body
         (setq ,(car spec) (1+ ,(car spec))))
       ,@(cdr (cdr spec)))))

引起我注意的是(end (nth 1 spec))。我认为这部分必须在运行时完成。当这在运行时完成时,意味着代码扩展不能在编译时完成。 为了测试它,我稍微修改了 dotimes 并对文件进行了字节编译。

(defmacro my-dotimes (spec &rest body)
  (declare (indent 1) (debug dolist))
  (let ((temp '--dotimes-limit--)
        (start 0)
        ;; This is my test
        (end (and (print "test: ")(print (nth 1 spec)) (nth 1 spec))))
    `(let ((,temp ,end)
           (,(car spec) ,start))
       (while (< ,(car spec) ,temp)
         ,@body
         (setq ,(car spec) (1+ ,(car spec))))
       ,@(cdr (cdr spec)))))

(provide 'my)

结果

(require 'my)
(my-dotimes (var 3)
  (print "dotimes"))

;; "test: "
;; 3
;; "dotimes"
;; "dotimes"
;; "dotimes"

确实,我的测试语句是在运行时完成的。 说到扩展:

(pp-macroexpand-expression
 '(my-dotimes (var 3)
    (print "dotimes")))

;; (let
;;     ((--dotimes-limit-- 3)
;;      (var 0))
;;   (while
;;       (< var --dotimes-limit--)
;;     (print "dotimes")
;;     (setq var
;;           (1+ var))))

令人惊讶的是我的测试部分丢失了。

那么 dotimes 在运行时扩展了吗?

如果是,是否意味着它失去了通用 macro 的优势,即 macro 与直接编写相同的代码一样快?

当解释器遇到 macro 具有运行时组件和时会做什么?

我的印象是您对包含宏定义的文件进行了字节编译,但没有对调用宏的文件进行字节编译?

它是 调用 宏得到扩展。

如果编译调用代码,则扩展发生在编译时;在加载时("eager" 宏扩展,仅在最近的 Emacs 版本中)如果加载的代码未编译;并且在 运行-time 如果急切扩展不可能或不可用。

显然,如果在 运行 时评估对宏的调用,则永远没有任何机会提前扩展它,因此扩展会在 运行 时立即发生。

编译时(或加载时)的宏扩展是可能的,因为宏参数是计算的。在扩展时计算 (nth 1 spec) 没有任何问题,因为 spec 的值是 未计算的 参数,它出现在对宏的原始调用中。

即当扩展 (dotimes (var 3)) 时,spec 参数是列表 (var 3),因此 (nth 1 spec) 在扩展时被评估为 3.

为了清楚起见(因为这里的数字可能会混淆问题),如果它是 (nth 0 spec) 那么它会计算为符号 var,特别是 not var作为变量的值(展开时不能成立)

因此 (nth 1 spec)——实际上任何对传递给宏的未评估参数的操作——都可以绝对在扩展时建立。

(编辑:等等,我们在 中介绍了这个)

如果您问如果宏在扩展时执行某些操作并且在 运行-time 时使用 dynamic 的值会发生什么,答案很简单它看到扩展时间值,因此扩展代码可能会根据扩展发生的时间而有所不同。没有什么可以阻止您编写具有这种行为的宏,但通常不建议这样做。

只是我的两分钱...

People always say macro is expanded at compile time and it is as fast as writing the same copy of code directly. Is this always true?

这并不总是正确的。

如果您编译的函数的代码中包含对宏的调用,则为真。

但是,如果你不编译它,它几乎是真的。

假设您使用 dotimes 的修改版本定义一个函数,例如:

(defun foo (x)
  (my-dotimes (var x)
    (print "dotimes")))

而你编译它。

如果你只调用一次 (foo 5),解释器将不得不在评估扩展代码之前扩展宏,所以你会看到打印结果,并且它会比你自己编写扩展代码慢在函数内部。

但是现在,符号my-times 不再出现在foo 的定义中了。包含它的列表已被其扩展结果替换。

因此,如果您再次调用 foo,例如 (foo 3),现在您将看不到打印结果,它会像您自己编写扩展代码一样快。