请教 Macro 的参数 eval 问题. lambda 表达式相关。

(defmacro test (exp)
  (let ((e (eval exp)))
    ;; how to expand
    (message "%s -> %s" exp e)))

(test (eval (lambda () (interactive) "next" (next-line)))) ;; 不报错

;; test 宏也是Eval两次, 下面的表达式也是Eval两次。两种效果为什么不一样呢?

(eval (eval (lambda () (interactive) "next" (next-line)))) ;; 会报错

会不会因为 宏扩展期间 是 dynamic-binding ? https://www.gnu.org/software/emacs/manual/html_node/elisp/Anonymous-Functions.html

Macro: lambda args [doc] [interactive] body…

This macro returns an anonymous function with argument list args, documentation string doc (if any), interactive spec interactive (if any), and body forms given by body.

Under dynamic binding, this macro effectively makes lambda forms self-quoting: evaluating a form whose CAR is lambda yields the form itself:

(lambda (x) (* x x)) ⇒ (lambda (x) (* x x))

Note that when evaluating under lexical binding the result is a closure object (see Closures).

下面的描述是存在问题的(感谢 LeBeth 指正,请看我的第二个回帖)

-----------------------------------------------------------------------------

对任意表达式求值都隐含了调用 eval 的意思,在 *scratch* buffer 中按下 C-x C-e 就是调用了一次 eval,在 ielm 中输入表达式然后按下回车也是这样。 如果 eval 接受的是表参数,那么它就需要通过表首元素来判断表是 lambda 表达式,还是调用形式,或者是其他的东西。对于 lambda 表达式来说,在动态绑定下 eval 是幂等的,下面的例子可以说明:

(setq lexical-binding nil)
(lambda (x) x) => (lambda(x) x) ;C-x C-e
(eval (eval (eval (lambda (x) x)))) => (lambda(x) x) 
; use lexical scope
(setq lexical-binding t)
(lambda(x) x) => (closure (t) (x) x) ;C-x C-e
(eval (lambda(x) x) => "[Error Message]: (void-function closure)"

在词法绑定下,对 lambda 表达式求值后得到闭包,若再次 eval,由于作为参数的闭包首元素是 closure 而不是 lambda,所以它认为是调用形式,自然会报错。

你在测试的时候使用的应该是默认的词法绑定,对最下面的那行求值相当于套了三次 eval(这里仅指看到的两个加上隐含的一个),第一次得到闭包,第二次以为是调用,所以出错。

至于宏为什么没有问题,那要么是只进行了一次 eval,要么是对 lambda 表达式的求值得到了本身而不是新的闭包。上面的宏参数里面有 eval,所以有两次顶层的 eval 操作,那就只可能是 (eval (lambda()…) 得到了 lambda 表达式而不是闭包。

这里我只能猜测 宏展开期间调用 eval 对函数表达式求值默认使用的是动态绑定规则 ,把上面的代码改成这样,宏调用也会报错了:

(defmacro test (exp)
  (let ((e (eval exp t)))
    ;; how to expand
    (message "%s -> %s" exp e)))

(test (eval (lambda(x) x))) => "(void-function closure)"

但是你要说凡是宏调用过程中都是使用的动态绑定形式的 eval貌似也不太对 :rofl: 下面是 elisp manual 上给的例子,用来说明在展开期调用 eval 的坏处


(setq lexical-binding nil)
(defmacro foo (a)
  (list 'setq (eval a) t))

(setq a 'c)
(foo a) → (setq a t)
     ⇒ t                  ; but this set a, not c.

上面这段代码在动态绑定下会把 a 赋为 t 而不是 c,这是因为调用宏时 a 的绑定是符号 a 而不是 c。 然而,如果你在词法绑定下对上述代码求值时,c 是可以正常赋值的。。。。。。

总结:不要在宏展开期间使用 eval

上面的代码在 emacs 27.1 on windows 下面可以正常工作。楼主算是帮我发现了一个盲点,光看文档还真是想不到会有这样的情况 :smile:

1 个赞
(eval '(eval (lambda () (interactive) "next" (next-line))))
;; => (lambda nil (interactive) "next" (next-line))

和宏展开用什么 binding 没关系。只是因为 eval 不给另外参数的话会用 dynamic binding 而己。

(eval '(eval (lambda () (interactive) "next" (next-line))) t)
;; Error (void-function closure)
(eval FORM &optional LEXICAL)

  Probably introduced at or before Emacs version 15.

Evaluate FORM and return its value.
If LEXICAL is t, evaluate using lexical scoping.
LEXICAL can also be an actual lexical environment, in the form of an
alist mapping symbols to their value.

另外用 lexical binding 下,(eval (lambda () (interactive) "next" (next-line))) 基本等于 (eval '(closure (t) nil (interactive) "next" (next-line)))。因为 Emacs Lisp 的 lambda 在不同 binding 下,只要没有 byte compile 过, (listp (lambda nil (interactive) "next" (next-line))) 都是 t

;; -*- lexical-binding: t -*-
(defun foo () (listp (lambda () t)))

(foo)
;; t

(byte-compile 'foo)
;; #[0 "\300<\207" [#[0 "\300\207" [t] 1]] 1]

(foo)
;; nil

也就是说,真正造成问题的原因是 lambda 在不同 lexical-binding 下的行为不一致。

2 个赞

解释清晰,正解。谢谢大佬的解惑。 :heart:

原来我一直理解的都是错的 :rofl:

关于我上面的帖子,我在这里做一些修正和补充:

首先是 eval 函数,它接受一个原子或表,并对其进行求值,返回求值结果。如果不给第二参数的话默认使用 dynamic binding 求值规则。当我们按下 C-x C-e 时就会调用 eval-last-sexp,它的内部(指 elisp–eval-last-sexp)也使用了 eval,它使用 lexical-binding 作为第二参数,这里截取一小段:

;; elisp-mode.el : elisp--eval-last-sexp

;; Setup the lexical environment if lexical-binding is enabled.
(elisp--eval-last-sexp-print-value
 (eval (macroexpand-all
        (eval-sexp-add-defvars (elisp--preceding-sexp)))
       lexical-binding)

当输入形如 (eval (eval ..)) 的东西并按下 C-x C-e 时,实际上等价于 (eval '(eval (eval ...)) lexical-binding)

这里举个简单例子说明一下不同求值规则的影响

(setq b 1)
(defun abc (x)
   (+ x b))

(list  (eval '(let ((b 2)) (funcall 'abc 1)))
       (let ((b 2)) (funcall 'abc 1)))
=> (3 2)

虽然最外层(指 C-x C-e)的 eval 是 lexical binding ,但是内部的 eval 不受其影响,上面出现的 eval 使用的是 dynamic binding,另一个表达式使用的是 lexical binding,得到了不同的结果。

至于 lambda 表达式,正如 LdBeth 所言,它是一个宏,参考源代码可知它的定义:

(defmacro lambda (&rest cdr)
  ;; Note that this definition should not use backquotes; subr.el should not
  ;; depend on backquote.el.
  ;; 省略一些声明表达式
  (list 'function (cons 'lambda cdr)))

它的作用就是将 (lambda ...) 变成 (function (lambda ...)) ,也就是 #'(lambda ...)

无论是在 dynamic 还是 lexical 下 (eval '(lambda (x) x)) 得到的都是 (lambda (x) x) ,这是因为将隐含的 eval 加入进来得到 (eval '(eval '(lambda(x) x)) lexical-binding) ,它的值就是内部 eval 求得的值。

但是 (eval (lambda (x) x) 就不行了,lambda 表达式的求值是直接受 lexical-binding 控制的。lambda 是个宏,展开后会在表首加上 function 关键字。当 lexical-binding 为 t 或 eval 的第二参数为 t 时,#'(lambda ...) 就会得到闭包,当 lexical-binding 为 nil 时就会得到 (lambda ...)。以下代码可以说明这一点:

(setq lexical-binding t)
(macroexpand-all '(eval (lambda (x) x))) => (eval #'(lambda (x) x))
(eval (lambda (x) x)) => "Error :(void-function closure)"

(setq lexical-binding nil)
(eval (lambda (x) x)) => (lambda (x) x)
(eval (lambda (x) x) t) => (closure (t) (x) x)

上面 LdBeth 说到 lexical-binding 影响 lambda 的行为,也许更为准确的说法是 lexical-binding 影响 function ,也就是 #' 的行为。

使用的环境还是 27.1

再加一句,之所以上面的

(test (eval (lambda () (interactive) "next" (next-line))))

可以正常运行,是因为宏不处理参数,在宏内部 (eval exp) 的时候 exp 里面的 lambda 没展开。

再补一句, (eval '(lambda (x) x) t) 得到的是闭包,它与 lexical-binding 的值无关,我猜可能是 eval 的特殊规则。

你是指在 29 上,emacs manual 上的那个例子使用两种 binding 结果都一样吗,我不太会在 windows 上编译 emacs,现在还没试过

Ver 29 on windows.

lexical-binding nil: (foo a) -> (setq a t)

lexical-binding t: (foo a) -> (setq c t)

可能需要在 lexical-binding 分别为 t 和 nil 时对宏再求一次定义,也就是说修改 lexical-binding 后再重新求值。看看这样是不是不一样。

因为宏在两个 binding 下对定义求值的时候得到的结果不一样,在 lexical-binding 下宏的函数是个闭包

是的。是这样的。

那就没问题了,在 lexical-binding 下宏函数是个闭包,函数传参的时候不会出现重绑定。全局 a 的值是 c,这里就得到 c 了。

如果在 dynamic-binding 下的话,全局 a 的值就是 a 了,之前的值被遮蔽了。

1 个赞

elisp的procedure居然是透明的,而且区分出两种procedure。

(eval '(let ((x 1))
         (lambda () (+ x 1))))
(lambda nil (+ x 1))
(eval '(let ((x 1))
         (lambda () (+ x 1))) t)
(closure ((x . 1) t) nil (+ x 1))

这个返回lambda 或者 closure是一个list,可以被进一步应用:

(let ((x 1))
  ((lambda nil (+ x 1))))  
2
((closure ((x . 1) t) nil (+ x 1)))
2

我认为这一点比Scheme好,Scheme里尽管也有透明record,却没有透明procedure。

让我有点吃惊的是:当byte compile之后,这个procedure就会变成了不透明。

这意味着编译前后,行为会不一样。

一个比较好的做法是,在底层仍然保留这个透明表现,但是在write的时候把它reify出来。

注:刚才试了一下SBCL,它的行为和Scheme一样,procedure也是不透明的。

不是完全透明的,我以前找到过反例

不过我当时对 lisp 理解还不够,这样写就可以了

(let ((p '(x . 1)))
  (setq foo
        `(closure (,p t) (y)
                  (setq x y)))
  (setq bar
        `(closure (,p t) ()
                  x)))

(funcall foo 2)
;; 2

bar
;; (closure ((x . 2) t) nil x)

(funcall bar)
;; 2

Emacs Lisp 的 printer 本身对 sharing 就不是很透明。

(setq print-circle t)

foo
;; (closure ((x . 2) t) (y) (setq x y))
(list foo bar)
;; ((closure (#1=(x . 2) t) (y) (setq x y)) (closure (#1# t) nil x))

SBCL 的设计是只有 compiler,没有 interpreter,所有对 eval 的调用是 just in time compile。这样就没有不一致的问題。然后因为和 CLOS MOP 的 funcallable instance 设计冲突没有透明的 closure 实现也很正常。

有些比较简易的 Scheme 的 closure 实现是透明的。我记很 festival TTS 用的 Scheme 就是这样 https://www.cstr.ed.ac.uk/projects/festival/manual/

Emacs Lisp 的 byte-compile 需要利用 lexical scoping 的 renaming 去除局部变量来相对 dynamic scoping 提高运行效率。

1 个赞

(setq print-circle t) 是个好东西,感谢。

我刚才试了一下letrec,在不使用(setq print-circle t) 的情况下,会打印出不太结构化的信息:

(eval '(letrec ((f (lambda (x) (if (> x 0) 0 1))))
         f) t)

;; (closure ((f closure #1 (x) (if ... 0 1)) t) (x) (if (> x 0) 0 1))

这里 ...不知道是什么东西(我还没真正用递归呢)。

在使用(setq print-circle t)后,其结果更符合预期:

(setq print-circle t)

(eval '(letrec ((f (lambda (x) (if (> x 0) 0 1))))
         f) t)

;; #1=(closure ((f . #1#) t) (x) (if (> x 0) 0 1))

然而,无论是否使用 (setq print-circle t),返回的(closure ...) 都是无法直接使用的。

一种可能的解决方法是让Emacs-Lisp支持 Reading Graph Structure


透明procedure能带来不少益处,比如:能以文本的形式对procedure进行序列化和反序列化。