Play with macro

May 25, 2007 – 11:15 pm

Lisp 的宏可谓是异常强大。我所接触过的宏大约算三种:

  • 一种是 C 语言的宏,这几乎可以算是功能最弱但又用得最多的宏了。只做非常简单的语法分析,并进行文本替换。但是实际上这种简单的宏为 C/C++ 带来了许多额外的能力,不过从来这个东西好像也没有专门的文献以及教材详细讲解,大多是经验丰富的程序员们通过源代码互相传播关于宏的知识,而且许多方面在各个不同的编译器上的结果都是不一样的,所以一直以来宏也只有那部分非常常见的用法为大家所广泛接收并使用。事实上,如果你感兴趣,可以去看一看 boost.preprocessor ,你会了解到其实宏可以做很多很多的事情。
  • 一种是 C++ 的模板,我把这看作一种宏,因为模板也是操纵代码,而并不在运行期存在。C++ 的模板比起原始的宏来说要强大了许多,它会进行语法分析,并且藉由篇特化等特性让它具有了许多意想不到的能力。我不知道 C++ 模板最初设计出来是不是有一点取代宏的初衷,不过现在看来似乎完全没有走在这个方向。模板和宏都有各自的用处。模板+内联函数+常量等特性可以在一定程度上取代宏,但是宏仍然有它生存的地方。而模板似乎是刚刚找到了自己真正的方向,发展出如今鼎鼎大名的“范型编程” (GP) 。我对模板也处于刚刚开始了解的状态,不过模板似乎还处在发展初期,不像 OOP 一类的技术,先有了一套已经研究透了的理论,才拿出来实现,模板似乎是无心插柳柳成荫,做出模板这个特性以后突然发现原来模板还有这样那样的用法,实现各种各样特性的方法被人们发掘出来,种种特性令人振奋!然而也正是因为初衷并不是用来做那些事情的吧,即使可以技巧性很强地实现那些范型技术,可是却有许多尴尬的地方,例如如果出现编译错误,往往错误信息是风马牛不相及,编译速度过慢,不方便查看模板扩展后的结果,不便于调试,而且模板的种种变态的用法甚至会轻松把编译器搞挂掉。因此,模板相关的技术都需要更进一步地发展,才能更广泛地投入工业使用啊,要不然就只有作为学院派的高级玩具了。明年 C++0x 就要出来了,也算是非常令人期待的了。
  • 最后就是 Lisp 的宏了。不管是老式的 C 宏还是 C++ 的新式模板,都是独立于原来的语言的另一种语言,他们运行于编译期(如果把预处理也包含在编译期内的话)。而 Lisp 的宏与它们的最大的区别就是,Lisp 宏与 Lisp 本身是相同的一种语言,完全相同,只不过宏运行于编译期。加上 Lisp 强大的表处理能力,就能对它自己进行随心所欲的控制了(Lisp 语言本身是由表组成的)。这往往让 Lisp 的宏成为独特的威力强大的完全区别于基于文本替换的宏以及更高级的模板等技术。

宏的一个常见的用途就是用于定义“新的语言”。Steve Bourne 在为 Unix Version 7 写 shell 的时候 (就是著名的 Bourne Shell) 曾经用宏让 C 语言“变成了” Algol-68:

1
2
3
4
5
6
7
8
9
10
11
#define STRING char *
#define IF if (
#define THEN ) {
#define ELSE } else {
#define FI ;}
#define WHILE while (
#define DO ) {
#define OD ;}
#define INT int
#define BEGIN {
#define END }

然后他这样写代码:

1
2
3
4
5
6
7
8
9
10
11
INT compare(s1, s2)
    STRING s1;
    STRING s2;
BEGIN
    WHILE *s1++ == *s2
    DO IF *s2++ == 0
        THEN return (0);
        FI
    OD
        return(*--s1 - *s2);
END

当然他这样的做法遭到众人的抗议,而且 Bourne Shell 的代码也一直被认为是难以维护的典范。我在这里举这个例子并不是为了说明使用宏让语言变成另外一个样子是不好的做法。事实上,我们经常需要这样做,小到 syntax sugar ,大到 DSL (Domain Specific Language) ,我们到处都在使用宏的这种特性。而 Lisp 的这种强大的宏让事情变得更加普遍,事实上,在 Common Lisp 里面,就有一个强大的 loop 宏,它非常灵活,采用了更类似于命令式语言的风格 (例如我们熟悉的 for , while 以及 return 等) ,而不是 Lisp 原本的 map 的风格 (也因此一直不为一些保守的 Lisper 们所接受) ,另外,还有专门的宏让你可以使用中缀表达式来写 Lisp 程序 (许多人把 Lisp 的前缀表达式作为拒绝的理由) ,等等。而在于 DSL 的领域, Lisp 也和通常的其他语言的做法不同,它采用一种自底向上的做法,在 Lisp 语言本身的基础上,通过宏和表操作,构建更高层的语言应用,结果 DSL 本身其实又是 Lisp ,只不过处在更高一层,但是仍然可以使用所有最底层的 Lisp 的功能。Paul Graham 在 Programming Bottom-Up 进行了阐述。

在写 Lisp 的宏的时候,通常只要写出期望的原来的样子和期望得到的结果的样子,剩下的工作就会变得很容易了。下面我用我刚刚在 Elisp 里面做的宏作为例子来简单地介绍一下。我在前一篇 Blog 里面介绍了 smart-snippet ,可以方便地为各个 mode 定义 snippet 。然而许多时候几个不同的 mode 可以定义相同的 snippet ,例如 c-modec++-modejava-mode 可以使用相同的 snippet 。然而原来的 snippet 代码不能方便地为几个不同的 mode 定义相同的 snippet 和 key-binding 。考虑实现这个功能,我希望我在定义的时候可以这样写:

1
2
3
4
5
6
7
(smart-snippet-with-abbrev-tables
 (java-mode-abbrev-table
  c++-mode-abbrev-table
  c-mode-abbrev-table)
 
 ("if" "if ($${condition})\n{$>\n$>$.\n}$>" 'bol?)
 ("else" "else\n{$>\n$>$.\n}$>" 'bol?))

来实现分别为三个 mode 定义两个 snippet 的功能,如果手写,会写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(progn
  (smart-snippet-abbrev
   'java-mode-abbrev-table
   "if"
   "if ($${condition})\n{$>\n$>$.\n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   'java-mode-abbrev-table
   "else"
   "else\n{$>\n$>$.\n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   'c++-mode-abbrev-table
   "if"
   "if ($${condition})\n{$>\n$>$.\n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   'c++-mode-abbrev-table
   "else"
   "else\n{$>\n$>$.\n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   'c-mode-abbrev-table
   "if"
   "if ($${condition})\n{$>\n$>$.\n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   'c-mode-abbrev-table
   "else"
   "else\n{$>\n$>$.\n}$>"
   'bol\?))

可以看到,首先,第一个参数是一个表,有点类似于和后面的参数做笛卡尔积的感觉。这需要一个双重嵌套的循环,没有问题,我们可以使用 loop 宏来完成,大概会像这个样子:

1
2
3
4
(defun double-loop (list1 &rest list2)
  (loop for i in list1
        collect (loop for j in list2
                      collect (list 'func i j))))

而调用 (double-loop '(1 2 3) 4 5) 就会得到这样的结果:

1
2
3
4
5
6
(((func 1 4)
  (func 1 5))
 ((func 2 4)
  (func 2 5))
 ((func 3 4)
  (func 3 5)))

得到了 (func 1 4) 这样的结构了,稍微改一下就可以用于定义单个的 snippet 。然而这里并不是我们想要的,虽然是嵌套循环,我们想要的是最后的表 (也就是 func 的函数调用) 需要处于同一层。于是我们需要一个 flatten-1 来去掉一层多余的表结构:

1
2
3
4
5
6
7
(defun flatten-1 (list)
  (cond ((atom list) list)
	((listp (car list))
	 (append (car list)
		 (flatten-1 (cdr list))))
	(t (append (list (car list))
		   (flatten-1 (cdr list))))))

调用 (flatten-1 (double-loop '(1 2 3) 4 5)) 就可以得到想要的结果了:

1
2
3
4
5
6
((func 1 4)
 (func 1 5)
 (func 2 4)
 (func 2 5)
 (func 3 4)
 (func 3 5))

下面就可以写做宏的形式:

1
2
3
4
5
6
7
8
9
10
11
(defmacro smart-snippet-with-abbrev-tables
  (abbrev-tables &rest snippets)
  `(progn
     ,@(smart-snippet-flatten-1
	(loop for table in abbrev-tables
	      collect (loop for snippet in snippets
			    collect (append
				     (list
				      'smart-snippet-abbrev
				      table)
				     snippet))))))

然后我们可以看一下效果,使用 M-x pp-eval-last-sexp 来执行下面的代码:

1
2
3
4
5
6
7
(macroexpand '(smart-snippet-with-abbrev-tables
 (java-mode-abbrev-table
  c++-mode-abbrev-table
  c-mode-abbrev-table)
 
 ("if" "if ($${condition})n{$>n$>$.n}$>" 'bol?)
 ("else" "elsen{$>n$>$.n}$>" 'bol?)))

就可以看到如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(progn
  (smart-snippet-abbrev
   java-mode-abbrev-table
   "if"
   "if ($${condition})n{$>n$>$.n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   java-mode-abbrev-table
   "else"
   "elsen{$>n$>$.n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   c++-mode-abbrev-table
   "if"
   "if ($${condition})n{$>n$>$.n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   c++-mode-abbrev-table
   "else"
   "elsen{$>n$>$.n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   c-mode-abbrev-table
   "if"
   "if ($${condition})n{$>n$>$.n}$>"
   'bol\?)
 
  (smart-snippet-abbrev
   c-mode-abbrev-table
   "else"
   "elsen{$>n$>$.n}$>"
   'bol\?))

已经很接近结果了,唯一的不足就是我们应该使用 'c-mode-abbrev-table 而不是没有 quote 的形式。这个也好办,我们写一个函数来为作为第一个参数的包含所有 abbrev-table 的表的每一个元素加上 quote

1
2
3
(defun smart-snippet-quote-element (list)
  (loop for item in list
	collect (list 'quote item)))

最后把这个函数加进去,就得到完整版的 smart-snippet-with-abbrev-tables 宏:

1
2
3
4
5
6
7
8
9
10
11
12
(defmacro smart-snippet-with-abbrev-tables
  (abbrev-tables &rest snippets)
  (let ((tables (smart-snippet-quote-element abbrev-tables)))
    `(progn
       ,@(smart-snippet-flatten-1
	  (loop for table in tables
		collect (loop for snippet in snippets
			      collect (append
				       (list
					'smart-snippet-abbrev
					table)
				       snippet)))))))

用同样的一些工具函数,我还做了 smart-snippet-with-keymap 宏,而且它们的结构都很类似,我甚至可以把这个结构抽象出来,定义一个新的宏,比如,叫做 smart-snippet-def-with ,然后使用这个宏来定义 with-abbrev-tablewith-keymap 宏,来达到代码重复程度最小化 (事实上,名如 def-xxx 的宏在 ELisp 里面是非常常见的) 。

当然,宏的用法并不局限于此。Paul Graham 在他的《On Lisp》一书中描述了大量 Lisp Macro 的技巧,如果感兴趣,可以找来阅读一下。

  1. One Response to “Play with macro”

  2. 这样做代码难以维护的

    By Macro on Oct 18, 2011

Post a Comment