Elisp 中的宏

Table of Contents


这里所说的宏,并非键盘宏。

例子

使用宏我们可以为 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 部分需要做些解释,首先:

  1. ` 的用法前面已有介绍, ,(...) 中的内容会在宏展开的时候执行,通过这一步可以生成函数名。
  2. 要理解和认识到代码中,那部分是需要在宏展开的时候执行
  3. macroexpand-1 可以对宏展开看下生成的函数是否正确。
  4. (school "小明" 78) 的用法,看起来就像是一个新的语法。宏的强大在于它确实可以自定义语法,但是也不要滥用,否则别人无法读懂你的代码。

参考