Emacs中诡异的宏
今天在写一个宏时, 碰到了一个很诡异的问题. 问题是这样的:
1 2 | (setq list '("C-x e" "C-x f")) (kbd (nth 0 list)) |
emacs却报错:
Debugger entered--Lisp error: (wrong-type-argument integer-or-marker-p (nth 0 list)) |
这么简单的一个程序, 却运行错误, 把我搞的莫名其妙, 弄了好久都不知道为啥会出错, 最后请教一位emacs前辈, 才搞明白了. 原因是: kbd是一个宏. 那为什么它是一个宏, 就会报那个错呢? 想要搞清楚这个原因的话, 我们先必须搞清楚emacs中宏与函数的异同点以及emacs中的宏与c中的宏的异同点。
Elisp中函数与宏的比较
- 相同点
函数调用的时候要对函数体进行求值,宏调用的时候要对宏体进行调用,比如:1 2 3 4 5
(defun test-fun (a b) (+ a b)) (defmacro test-macro (a b) (+ a b))
(test-fun 1 2) => 3
(test-macro 1 2) => 3
这点上它们是相同的 - 不同点
- 函数是对参数进行求值以后才传到函数体里面去的,而宏不求值直接作为一个symbol传到宏体里面去的,这点很重要,如果没有理解好这点,会犯好多错误,我们来看看下面的例子:
1 2 3
(test-fun 1 (+ 2 3)) => 6 (test-macro 1 (+ 2 3)) emacs报错: Debugger entered--Lisp error: (wrong-type-argument number-or-marker-p (+ 2 3))
为什么调用(test-macro 1 (+ 2 3))的时候emacs会报错呢, 原因就是因为对宏求值的时候, 直接把参数做为宏的参数, 并不求值, 我们来看一下emacs报的堆栈提示就知道了:
1 2 3 4 5 6 7 8
Debugger entered--Lisp error: (wrong-type-argument number-or-marker-p (+ 2 3)) +(1 (+ 2 3)) (lambda (a b) (+ a b))(1 (+ 2 3)) (test-macro 1 (+ 2 3)) eval((test-macro 1 (+ 2 3))) eval-last-sexp-1(nil) eval-last-sexp(nil) call-interactively(eval-last-sexp nil nil)
从上面, 很明显就可以看出没有先对(+ 2 3)求值, 就直接作为参数传给宏了.
- 函数对函数体求值后, 就完事了, 宏不同, 它对宏体求值后, 还要对求值后的表达式再求值一遍, 我们来看下面的例子:
1 2 3 4 5
(defun test-fun (a b) (list '+ a b)) (defmacro test-macro (a b) (list '+ a b))
(test-fun 1 2) => (+ 1 2)
(test-macro 1 2) => 3
为什么它们的结果不同? 原因就是elisp对宏体求值后, 对求值后的结果再求值一遍, 即: 对宏体求值得到(+ 1 2), 这个和(test-fun 1 2)结果是一样的, 但是此时elisp解释器并没有止步, 还要进一步对(+ 1 2)行求值, 进而最终得到3.
- 函数是对参数进行求值以后才传到函数体里面去的,而宏不求值直接作为一个symbol传到宏体里面去的,这点很重要,如果没有理解好这点,会犯好多错误,我们来看看下面的例子:
Elisp中的宏与c中的宏的异同点
- 相同点 它们都是不对参数进行求值, 就直接作为参数传给宏.
- 不同点 c中的宏是直接进行宏替换, 然后对替换后的表达式进行求值, 而且这个是在预处理阶段进行的, 而elisp中的宏则是对宏体进行求值后, 再进行宏扩展, 它则是在程序运行过程中进行的. 来看下面的例子:
1 2 3
#define test_macro_c(a, b) printf("%d%d", a, b) (defmacro test-macro-elisp (a b) (list '+ a b))
test_macro_c(1, 1+2)进行宏扩展后变为printf(“%d%d”, 1, 1+2)
test-macro-elisp(1, 2)并不是像c中那样简单的扩展变为(list ‘+ 1 2), 然后再对(list ‘+ 1 2)行求值, 最终得到结果(+ 1 2), 而是先对宏体进行求值得到(+ 1 2), 然后再进行宏展开, 即对刚才的结果进行再求值, 最终得到3.
经过上面的分析, 我相信对于文中开头提出的那个问题, 我们很容易就弄清楚原因了.
我们先来看看kbd的定义:
1 2 3 4 5 | (defmacro kbd (keys) "Convert KEYS to the internal Emacs key representation. KEYS should be a string constant in the format used for saving keyboard macros (see `edmacro-mode')." (read-kbd-macro keys)) |
(kbd (nth 0 list)), 先对宏体进行求值, 即对(read-kbd-macro (nth 0 list))进行求值, 注意: 此时(nth 0 list)没有被求值, 只是作为宏参数传进来了, 它并不是read-kbd-macro所要求的参数, 所以最终出现了那个错误. 我们来看一下emacs的堆栈信息, 也能了解到这点:
1 2 3 4 5 6 7 8 | Debugger entered--Lisp error: (wrong-type-argument integer-or-marker-p (nth 0 list)) read-kbd-macro((nth 0 list)) (lambda (keys) (read-kbd-macro keys))((nth 0 list)) (kbd (nth 0 list)) eval((kbd (nth 0 list))) eval-last-sexp-1(nil) eval-last-sexp(nil) call-interactively(eval-last-sexp nil nil) |
如果想要刚才的代码能运行, 可以把kbd的定义改为:
1 2 | (demacro kbd (keys) `(read-kbd-macro ,keys)) |
这样,先对宏体求值得到(read-kbd-macro “C-x e”), 然后再求值就可以了.
虽然emacs的宏很诡异, 但是能实现一些比较强大的功能, 例如:
1 2 3 4 5 6 7 8 | (defmacro def-remember-command (command) "Make definition of command which remember `poiint'." `(defun ,(concat-name command "-remember") () ,(concat "When `" command "' remember `point'.") (interactive) (call-interactively (intern ,command)) (if mark-active (setq last-region-end (point))))) |
调用(def-remember-command “next-line”), 先对宏体求值得到:
1 2 3 4 5 6 7 | (defun next-line-remember nil "When `next-line' remember `point'." (interactive) (call-interactively (intern "next-line")) (if mark-active (setq last-region-end (point)))) |
然后再对求值后的结果再求值, 最终实现定义函数next-line-remember的功能.

loading...
很好的学习贴,实用
[回复]
没看懂关于C和elisp的宏的区别,onlisp上写先展开,执行的时候求值,这不是和c一样么?是我没看懂,还是 elisp和common lisp不一样?
[回复]
ahei 回复:
一月 4th, 2011 at 10:41 下午
@Lowe,
,关键理解这个例子:
[回复]
Lowe 回复:
一月 4th, 2011 at 11:03 下午
@ahei, 就是说lisp里面的两步,展开->求值,第一步展开不是c里面的替换,而是替换之后求值?
[回复]
ahei 回复:
一月 4th, 2011 at 11:15 下午
@Lowe, en
[回复]
有好多看起来像 typo 的地方。麻烦 ahei 修改一下,方便我们准确理解。
[回复]
ahei 回复:
一月 5th, 2011 at 2:51 下午
@小和平鸽, 哪typo啦?
[回复]
小和平鸽 回复:
一月 5th, 2011 at 3:01 下午
@ahei, 我的意思是这个地方。不过又仔细看了一遍以后发现应该没有敲错。不过这个地方看起来是比较绕。能有一个更容易理解的例子就好了。
test_macro_c(1, 1+2)进行宏扩展后变为printf(“%d%d”, 1, 1+2)
test-macro-elisp(1, 2)并不是像c中那样简单的扩展变为(list ‘+ 1 2), 然后再对(list ‘+ 1 2)行求值, 最终得到结果(+ 1 2), 而是先对宏体进行求值得到(+ 1 2), 然后再进行宏展开, 即对刚才的结果进行再求值, 最终得到3.
[回复]
ahei 回复:
一月 5th, 2011 at 3:04 下午
@小和平鸽, 这个确实是很绕的,你可以看info里面的解释, 那里面写的也挺绕的,只能自己好好意会了
[回复]
(demacro kbd (keys)
`(read-kbd-macro ,keys))
这样改后,怎么运行还是有错?
[回复]