Elisp 中的宏
这里所说的宏,并非键盘宏。
例子
使用宏我们可以为 Elisp 定义新的语法特性,但本质上它们其实是些语法糖。
例如,when 的实现:
(defmacro when (cond &rest body)
(declare (indent 1) (debug t))
(list 'if cond (cons 'progn body)))
其中, 在宏定义里可以引入 declare 表达式,它可以增加一些信息。目前只支持两类声明:debug 和 indent。 indent 的类型比较简单,它可以使用这样几种类型:
- nil 也就是一般的方式缩进
- defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进
- 整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1
- 符号 这个是最坏情况,你要写一个函数自己处理缩进。
具体可以看 Indenting Macros。
下面我们就可以使用 when 了:
(setq x 1)
(setq y 2)
(when (< x y)
(insert "\nHello world"))
我们可以使用 macroexpand 来展开宏:
(macroexpand '(when (< x y)
(insert "\nhello world")
))
结果如下:
(if
(< x y)
(progn
(insert "\nhello world")))
数据和代码
使用宏,我们可以像操纵数据一样操纵代码。
例如,写一个 inc 函数,它会自动将 var 加 1:
(defun inc (var)
(list 'setq var (list '1+ var)))
(inc 'x)
注意这里的符号 '
,直接写 x 表示变量或者函数,加引用 'x
表示符号(symbol)。如果想理解下面的部分,要对 symbol 这种类型有很好的了解。可看Lisp 之 Symbol
会输出如下的结果:
(setq x
(1+ x))
我们发现,这就是一段代码啊。所以,我们可以这样:
(setq x 1)
(eval (inc 'x)) ⇒ 结果为 2
如果我们使用宏来实现呢?
(defmacro inc (var)
(list 'setq var (list '1+ var)))
然后我们可以直接这样使用与上面通过函数的方式效果一样:
(setq x 1)
(inc x)
函数和宏
宏的参数是出现在最后扩展后的表达式中,而函数参数是求值后才传递给这个函数。 例如下面的例子:
(defun test1 (var)
(message "%d %d" var var))
(defmacro test2 (var)
(list 'message "%d %d" var var))
(setq x 1)
(test1 (inc x)) ;; 结果为 "2 2"
(test2 (inc x)) ;; 结果为 "2 3"
原因是 (test2 1) 被展开为 (message "%d %d" (inc x) (inc x))。
反引用
为了简化宏的定义,我们可以使用一个特殊的宏 `
,这个键位于 tab 的上面。
如果使用了 ` 所有的元素都是引起(quote)的,例如:
`(hello world) ;; 相当于 (list 'hello 'world)
在这种用法下,如果让一个表达式不引起,需要在前面加 ,
,如果要让一个列表作为整个列表的一部分(slice),可以用 ,@
。
例如上面定义的 when 就可以这样来写:
(defmacro when (cond &rest body)
`(if ,cond
(progn ,@body)))
生成宏的宏
如果要生成的宏为这个:
(defmacro inc (var)
`(setq ,var (1+ ,var)))
我们可以这样写:
(defmacro create-inc ()
`(defmacro inc (var)
`(setq ,var (1+ ,var))))
但是写这种宏,要注意嵌套反引用的用法。
一个实际应用
下面这段代码来自于 prelude 的配置。我们从这里能够很好的学到宏的使用方法,以及在何时使用威力更大。 在 SICP 中讲到了高阶程式(higher-order procedures),这段代码其实就是很好的一个范例。所谓的高阶程式,其实就是处理或操作其他程式的程式。
下面的代码其实就是通过宏来生成 3 个搜索函数。它们的区别仅仅在于 url 和提示信息不同,其他完全相同。它的优点不言而喻,极大的减少了无聊重复的代码;另外一个优点是它的扩展性很强,我们可以任意的添加其它的搜索函数。
(defun prelude-search (query-url prompt)
"Open the search url constructed with the QUERY-URL.
PROMPT sets the `read-string prompt."
(browse-url
(concat query-url
(url-hexify-string
(if mark-active
(buffer-substring (region-beginning) (region-end))
(read-string prompt))))))
(defmacro prelude-install-search-engine (search-engine-name search-engine-url search-engine-prompt)
"Given some information regarding a search engine, install the interactive command to search through them"
`(defun ,(intern (format "prelude-%s" search-engine-name)) ()
,(format "Search %s with a query or region if any." search-engine-name)
(interactive)
(prelude-search ,search-engine-url ,search-engine-prompt)))
(prelude-install-search-engine "google" "http://www.google.com/search?q=" "Google: ")
(prelude-install-search-engine "youtube" "http://www.youtube.com/results?search_query=" "Search YouTube: ")
(prelude-install-search-engine "github" "https://github.com/search?q=" "Search GitHub: ")
基于上面的代码,我们写一个简单点的:
(defmacro school (name score)
`(defun ,(intern (format "name_%s" name)) ()
(message (format "%s" ,score))
))
(macroexpand-1 '(school "xm" 30))
(school "小明" 78) ;; create new function => name_小明
(school "小赵" 89) ;; create new function => name_小赵
(name_小明) ==> "78"
(name_小赵) ==> "89"
我们定义了一个 school 的宏,通过它可以生成,例子中的 name_小明
, name_小赵
这样的函数。
对 defmacro 部分需要做些解释,首先:
`
的用法前面已有介绍,,(...)
中的内容会在宏展开的时候执行,通过这一步可以生成函数名。- 要理解和认识到代码中,那部分是需要在宏展开的时候执行
macroexpand-1
可以对宏展开看下生成的函数是否正确。(school "小明" 78)
的用法,看起来就像是一个新的语法。宏的强大在于它确实可以自定义语法,但是也不要滥用,否则别人无法读懂你的代码。