elisp宏扩展局部变量

elisp macro expansion local variable

我最近接触了 elisp 并尝试了解 elisp 宏的工作原理。 GNU 教程有一个 chapter Surprising-local-Vars 用于宏局部变量,我对宏扩展的工作原理感到困惑。

(defmacro for (var from init to final do &rest body)
  "Execute a simple for loop: (for i from 1 to 10 do (print i))."
  (let ((tempvar (make-symbol "max")))
    `(let ((,var ,init)
           (,tempvar ,final))
       (while (<= ,var ,tempvar)
         ,@body
         (inc ,var)))))

有两种 let 形式。第一个

(let ((tempvar (make-symbol "max")))

没有反引号,它将在宏扩展短语处进行评估,因此未插入的符号 "max" 将仅在该短语处创建。 而且uninterned符号"max"会在运行时丢失,应该不行吧?

但实际上,效果很好。我尝试了以下方法:

(for max from 1 to 10 do (print max))

其扩展如下:

(macroexpand '(for max from 1 to 10 do (print max)))

(let ((max 1) (max 10)) (while (<= max max) (print max) (setq max (+ 1 max))))

这里有两个max符号,一个绑定1,一个绑定10,while形式的表达式有两个max符号。

(while (<= max max)

while形式如何解析两个不同的符号"max"?

您正在查看打印名称,而不是符号标识。您有两个符号,都带有 "print name" max,但身份不同。通常,我会建议使用 gensym 而不是 make-symbol,但使用哪种方式并不重要。

将符号视为指向一个小结构的指针,其中存储了各种值。其中之一是 "name",当一个符号被驻留时,它被放置在一个特殊的结构中,所以你可以通过它的名字找到这个符号。您看到的是一个名为 max 的 interned 符号和一个名为 max 的 un-interned 符号。它们是不同的符号(即两个结构,因此指针不同),但是当您只看打印表示时,这并不明显。

快速演示,刚从 emacs“scratch”缓冲区中取出:

(defmacro demo (sym)
  (let ((tmp (make-symbol "max")))
    `(progn
       (message "%s" (list ',tmp ',sym))
       (eql ',tmp ',sym))))

demo

(macroexpand '(demo max))
(progn (message "%s" (list (quote max) (quote max))) (eql (quote max) (quote max)))

(demo max)
nil

如果您粘贴宏展开产生的文本并计算它,您会看到您得到的是 t 而不是 nil,因为在读取表达式的过程中,您结束了使用 相同的 符号。

简短的回答是 (make-gensym "max") 创建一个名称为 max 的新符号。您还使用了名称为 max 的符号。它们具有相同的名称,因此,在 Emacs 中,print 的方式相同,但它们不是相同的符号。我们可以通过创建一个宏来轻松测试这一点,该宏会创建一个符号和 returns 一个将其与宏参数进行比较的形式:

(defmacro test-gensym (arg)
  (let ((max (make-symbol "max")))
    `(eq ',max ',arg)))

如果我们查看宏展开,我们可以看到我们正在比较两个名称相同的品种:

(print (macroexpand '(test-gensym max)))
;;=> (eq (quote max) (quote max))

但如果我们实际上 运行 该代码,我们将看到被比较的值 不同:

(test-gensym max)
;;=> nil

与Common Lisp的对比:这里可以看到Common Lisp打印机让变量看起来不一样。一个是你的max,另一个是未实习的#:max

CL-USER 70 > (pprint (macroexpand '(for max from 1 to 10 do (print max))))

(LET ((MAX 1) (#:MAX 10))
  (LOOP WHILE (<= MAX #:MAX) DO (PROGN (PRINT MAX) (INCF MAX))))

如果我们告诉 Common Lisp 打印机支持打印循环数据结构,那么打印机会在未插入符号相同的地方进行标记:#1=#:MAX 是带有打印机标签的符号。 #1# 然后是对标记的事物的引用。

CL-USER 71 > (setf *print-circle* t)
T

CL-USER 72 > (pprint (macroexpand '(for max from 1 to 10 do (print max))))

(LET ((MAX 1) (#1=#:MAX 10))
  (LOOP WHILE (<= MAX #1#) DO (PROGN (PRINT MAX) (INCF MAX))))

但那是 Common Lisp,而不是 Emacs Lisp - 尽管它们是相关的,因为它们都是早期 Lisp 方言的继承者。