C 语言不具备匿名函数功能,但是 Vala 想办法模拟了一个[1]。我一直想用 C 的宏模拟一个,但是技拙,或许根本就无法实现。
GCC 提供了一个扩展,可以实现匿名函数[2]。例如:
#define lambda(return_type, function_body) \
({ return_type fn function_body fn })
这个 lambda
宏的用法如下:
lambda (int, (int x, int y) { return x > y; })(1, 2)
结果会被展开为:
({ int fn (int x, int y) { return x > y } fn; })(1, 2)
虽然挺不错,但它不是 C 标准。
最近断断续续的看了一些 GNU M4 的文档,脑洞略微开放了一些,便尝试用 M4 来模拟匿名函数。
原理
匿名函数并非真正的匿名。任何一个函数,它都需要在内存中有一个入口,即使那些支持匿名函数的编程语言也不例外。这个入口本质上就是一个内存地址。C 语言之所以不支持匿名函数,是因为 C 编译器要求每个函数的入口地址都必须绑定到一个名字上,这个名字就是函数名。那些支持匿名函数的编程语言,它们的编译器或解释器在发现匿名函数的存在时,它们或许暗自为之生成一个名字,或许直接就跳到函数的入口地址了。
不过,我对编译原理近乎无知,上述仅仅猜测。
如果在 C 语言中要模拟匿名函数,那么就必须想个办法偷偷的生成一些有名字的函数,然后用这个函数的调用来代替匿名函数的调用。例如下面这段假想的代码:
lambda(int, (int x, int y){ return x > y; })(1, 2);
应该想个办法将它变换为:
static int lambda(int x, int y){return x > y;}
lambda(1,2);
也就是先定义一个 lambda
函数,然后再调用它。如果程序中有多个匿名函数,一个 lambda
名字显然是不够用的,这就需要给 lambda
增加后缀名。例如:
lambda(int, (int x, int y){ return x > y; })(1, 2);
lambda(float, (float x, float y){ return x > y; })(1.0, 2.0);
应该变换为:
static int lambda_1(int x, int y){return x > y;}
static float lambda_2(float x, float y){return x > y;}
lambda_1(1,2);
lambda_2(1.0, 2.0);
就这么简单。事实上,Vala 对匿名函数的模拟也就是这么做的。
难点
这些 lambda_x
函数们的定义位置应该如何自动确定,是整个模拟过程中最难解决的问题。最佳的位置应该在 #include
之后,这样它们就对任何调用它们的函数都是可见的。
例如:
#include <stdio.h>
int main(void)
{
if(lambda(int, (int x, int y){return x > y;})(1, 2)) {
printf("False!\n");
}
}
应该被变换为:
#include <stdio.h>
static int lambda_1(int x, int y){return x > y;}
int main(void)
{
if(lambda_1(1, 2)) {
printf("True!\n");
} else {
printf("False!\n");
}
}
M4 的转移大法
要克服上述难题,可以用 M4 的 divert 宏。
divert 宏可以开启临时空间(临时文件),将当前的信息写入该空间,并且该空间的信息对于 divert 开启的其他临时空间是不可见的。
遵循 POSIX 标准的 M4,divert 最多能开启 10 个临时空间,编号从 0 到 9。GNU M4 对空间数量不做限制。不过 0 号临时空间是 m4 默认的工作空间。也就是说,m4 默认总是在 0 号临时空间中对宏进行展开处理,如果想让 m4 的工作空间转移到其他空间,就需要显式的调用 divert 宏进行转移。
例如:
我在 0 号空间
divert(1)dnl
现在我在 1 号空间了
divert(0)dnl
现在,我又回到了 0 号空间。
m4 的处理结果是:
我在 0 号空间
现在,我又回到了 0 号空间。
现在我在 1 号空间了
观察这个示例,我们可以得出以下规律:
某个空间中的信息是不断累积的,上例中的 0 号空间的信息累积到了一起。
非 0 号空间中的信息最终会被被放到 0 号空间中的信息之后。
再来一个略微复杂一点的例子:
divert(2)dnl
我在 2 号空间
divert(0)dnl
我在 0 号空间
divert(1)dnl
现在我在 1 号空间了
divert(0)dnl
现在,我又回到了 0 号空间。
m4 的处理结果是:
我在 0 号空间
现在,我又回到了 0 号空间。
现在我在 1 号空间了
我在 2 号空间
观察这个结果,我们又得出一个结论:非 0 号空间中的信息最终会依序被放到 0 号空间中的信息之后,先放 1 号空间的,然后放 2 号空间的,以此类推。
也就是说,一切空间,它们所包含的东西,最终都会跌落到 0 号空间,而且序号越大的空间,它向 0 号空间的跌落的优先级越低。
划分 C 代码的疆域
为了迎合 lambda 的定义与调用层面的分离,不得不对 C 代码划分疆域。最粗略的划分,可以将 C 代码划分为三个区域:
C_HEAD 区:所有的
lambda_x
函数定义之前的区域,对应divert(0)
,可以放置#include
之类的内容;C_LAMBDA 区:存放
lambda_x
函数们的定义,对应divert(1)
;C_BODY 区:存放常规 C 函数的定义,对应
divert(2)
。
这样,无论是 lambda 函数的定义,还是普通 C 函数的定义,它们最终会正确的跌落到 0 号空间中。
故事刚刚开始
我已经做了太多的铺垫,现在言归正传。不过,下面我要动用文式编程手段了,可能你不懂何为文式编程,而且也根本没啥兴趣去了解它,也没关系,但是你至少需要阅读『noweb 的用法』一文。
首先,我们要跳到一个序号为 -1
的空间:
<<-1 空间>>=
divert(-1)
@
虽然 -1
空间不是正规的 M4 空间,但是任何 M4 都会支持这个空间。-1
空间也会跌落到 0 号空间,但是它跌落到 0 号空间后就变成了一种有质无形的东西,或者你也可以说它是 0 号空间通向另一个世界的出口。用《黑客帝国》里的来将,序号不小于 0 的空间都是矩阵空间,而 -1
空间是锡安。-1
空间里的信息会能影响到序号不小于 0 的空间,而反过来却不行。在 M4 的世界里,没有史密斯这种怪物,而尼欧却可以在 -1
空间中插上电线进入序号不小于 0 的空间……
M4 之所以要制造 -1
空间,是因为 -1
空间中的任何内容不会被 m4 回显,而其他空间的信息最终都会被 m4 以某种形式回显出来。例如,即使 0 号空间的一个宏的定义:
define(`foo', `bar')
它也会被 m4 回显为一个空的字符。如果宏的定义之后存在换行符,那么这个换行符也会被 m4 如实的回显出来。因为在 m4 眼里,宏也是文本,文本也是宏。只有 -1
空间能够逃脱 m4 的集权统治,-1
空间,M4 的锡安!
脏乱差的锡安
矩阵里的城市,干净又简洁。锡安城里的一切都是脏乱差。M4 的 -1
空间也永远是这个样子。但是,没有脏乱差的锡安,矩阵也就无法再进化。
我们在 -1
空间中做的第一件事,就是为上一节所划分的三个 C 代码区域命名,也就是定义三个名称较为友好的 M4 宏:
<<-1 空间>>=
define(`C_HEAD', `divert(0)')
define(`C_LAMBDA', `divert(1)')
define(`C_BODY', `divert(2)')
@
然后,考虑如何为每个匿名函数取一个『独一无二』的名字。当然,这理论上是不可能的,因为 C 语言的命名空间是全局的。所以我们只能假定在一个 .c 文件内,除了匿名函数可以使用 lambda_x
这样的名字之外,其他函数不会用这样的名字。所以,现在我们要解决的问题就是如何为 lambda
增加后缀 _x
,并使得 x
的取值范围为整数集。
M4 不支持变量,但是可以利用宏定义模拟变量及其赋值操作:
<<定义全局变量 N>>=
define(`_set', `undefine(`$1')define(`$1', `$2')')
_set(`N', 0)
@
在此,我定义了一个可以对变量进行赋值的宏 _set
,然后用它将一个(全局)变量 N
『赋值』为 0
。实际上就是在 _set
宏中取消 N
的定义,然后重新定义一个 N
。这里,_set
是『返回』一个宏的宏,可以称之为『高阶宏』。
接下来,就是定义一个可以定义匿名函数的宏 lambda_define
:
<<C 的匿名函数模拟>>=
define(`lambda_define', `static $2 lambda_`'N`('$1`)'{$3;}')
@
这个 lambda_define
宏的用法如下:
lambda_define(`int x, int y',
`int',
`return x > y')
这个宏的第一个参数,是匿名函数所接受的形参,第二个参数是匿名函数的返回值类型,第三个参数是匿名函数体。 m4 会将这个宏展开为:
static int lambda_0(int x, int y){return x > y;}
之所以是 lambda_0
,是因为前面已经将 N
的初值设为 0
了。
接下来,就是定义 lambda
宏,它可以帮助我们产生匿名函数的定义,并在相应的位置调用这个匿名函数:
<<C 的匿名函数模拟>>=
define(`lambda',
`_set(`N', incr(N))`'dnl
`'`'`'`'C_LAMBDA`'lambda_define(`$1',`$2',`$3')
`'`'`'`'C_BODY`'lambda_`'N`'')
@
lambda
宏的定义,看似复杂,实则简单。复杂之处是一堆空的成对引号——`'。只需将这些空的成对的引号理解为没有宽度的『空格』即可。本来是没必要用这些么多成对的空引号的,但是用它们,一是为了安全,防止某个宏的展开结果污染了别的宏,二是为了第三、四行代码的『缩进』。如果直接用空格或 Tab 键,m4 会将它们如实的回显出来,这显然不是我想要的。dnl
宏的作用也是为了代码的美观——让宏定义代码换行但是又可以避免 m4 的输出结果中引入多余的换行符。
如果克服了空的成对引号与换行恐惧症,那么 lambda
的定义就相当简单了,它首先让 N
的值增 1,这是利用 M4 提内建的宏 incr
实现的。然后转移到 C_LAMBDA
空间,调用 lambda_define
宏,产生 C 匿名函数的定义代码。最后,从 C_LAMBDA
空间转移到 C_BODY
空间,产生 C 匿名函数的调用代码。
最后,我们将这些脏乱差的代码统统扔到 -1
空间:
<<-1 空间>>=
<<定义全局变量 N>>
<<C 的匿名函数模拟>>
@
回到 Matrix
想必你已经受够了 -1
空间中的那些诡异的 M4 代码。现在,在后脑勺上插上线,回到矩阵的世界吧。再见,锡安!C_HEAD
就是矩阵的入口,也就是 0 号空间的入口。
<<c-lambda.m4>>=
<<-1 空间>>
C_HEAD`'dnl
@
现在,我要严肃,不再使用黑客帝国的比喻。从文式编程的角度来看,上文中所有的 M4 代码现在都被扔到了一个名为 c-lambda.m4 的文件中了。
如果你将本文所有内容复制到一份文本文件中,假设这份文本文件叫作 c-lambda.nw,然后使用 noweb 产生 c-lambda.m4 文件。所用的命令如下:
$ notangle -Rc-lambda.m4 c-lambda.nw > c-lambda.m4
试试看
现在尝试在 C 代码中调用 c-lambda.m4 中定义的 lambda
宏是否可用。这需要新建一份 M4 文件,然后由这份 M4 文件产生一份 .c 文件。也就是说,这时你应该将 M4 视为 C 代码的生成器,也可以视为在用 M4 进行元编程。
假设新建的这份 M4 文件为 test-lambda.m4,它与 c-lambda.m4 文件在同一目录。用文式编程的手法可将 test-lambda.m4 文件抽象的表示为:
<<test-lambda.m4>>=
<<C_HEAD 区域>>
<<C_BODY 区域>>
@
为什么没有 C_LAMBDA
区域?因为这个区域是我们要悄悄生成的……否则,上文中的工作就白做了。
在 C_HEAD
区域,首先应该加载 c-lambda.m4 文件,因为我们所需要的 lambda
宏实在这份文件中定义的。
<<C_HEAD 区域>>=
include(`c-lambda.m4')dnl
@
之所以用 dnl
,是因为不希望这行语句在 m4 的回显结果中引入多余的空格。在 M4 中,dnl
的主要任务就是这个。注意,由于 c-lambda.m4 文件的末尾已经调用了 C_HEAD
宏,将 M4 的空间切换到了 0 号空间,所以这里就不需要再调用 C_HEAD
了。
C_BODY
区域需要调用 `C_BODY' 宏进行空间切换:
<<C_BODY 区域>>=
C_BODY
@
然后在 main
函数中寻找一个机会『定义』并『应用』一个匿名函数:
<<C_BODY 区域>>=
int main(void)
{
if(lambda(`int x, int y', `int', `return x > y')(1, 2)) {
printf("False!\n");
} else {
printf("True!\n");
}
}
@
既然 C_BODY 区域
调用了 printf
函数,那么 C_HEAD 区域
必须要 #include <stdio.h>
:
<<C_HEAD 区域>>=
#include <stdio.h>
@
现在,将本文的全部内容复制到一份文本文件 test-lambda.nw 中,然后执行以下命令即可生成 test-lambda.m4 文件,进而生成 test-lambda.c 文件:
$ notangle -Rtest-lambda.m4 test-lambda.nw > test-lambda.m4
$ m4 test-lambda.m4 > test-lambda.c
得益于管道,上述的两行命令与
$ notangle -Rtest-lambda.m4 test-lambda.nw | m4 > test-lambda.c
所得 test-lambda.c 的内容如下:
#include <stdio.h>
static int lambda_1(int x, int y){return x > y;}
int main(void)
{
if(lambda_1(1, 2)) {
printf("False!\n");
} else {
printf("True!\n");
}
}
发生了什么?
在 test-lambda.m4 文件中,首先是
include(`c-lambda.m4')
它会让 c-lambda.m4 中的 -1
空间中的内容进入 C_HEAD 区域(0 号空间)并且不会被 m4 回显。然后继续 C_HEAD 区域写出 `#include <stdio.h>' 这样的代码,这些代码会被 m4 首先回显出来,因为它们处于可回显空间的最底层。
当 m4 遇到 C_BODY
宏时,它就转移到 C_BODY
空间(2 号空间)去工作了。当它在 C_BODY
空间中遇到 lambda
宏,它转到 C_LAMBDA
空间(1 号空间)去产生匿名函数的定义代码,然后又回到 `C_BODY' 空间继续工作。
最后,当 m4 处理到 test-lambda.m4 的尾部,它会先让 C_LAMBDA
空间里的信息跌落到 C_HEAD
空间,然后再将 C_BODY
空间里的信息跌落到 C_HEAD
空间,最后再将C_HEAD
空间中所有的信息都回显出来,这样就产生了 test-lambda.c 文件中的内容。
也许,C_WORLD
这个名字要比 C_HEAD
更达意。
讨论
这篇文档并非炫耀如何把手捆住,以示我用脚也可以写字。它仅仅是我学习 GNU M4 的一个很小的练习,顺便以文式编程的方式将思路与实现详细的记录了下来。
另外,C 语言也不需要匿名函数。因为 C 语言的表现力太差,像下面这样的代码:
include(`c-lambda.m4')
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
C_BODY
int main (void)
{
char *str_array[5] = {"a", "abcd", "abc", "ab", "abcde"};
qsort (str_array, 5, sizeof (char *), lambda(`const void *s1, const void *s2',
`int',
`char *str1 = *(char **)s1;
char *str2 = *(char **)s2;
size_t l1 = strlen (str1);
size_t l2 = strlen (str2);
if (l1 > l2)
return 1;
else if (l1 == l2)
return 0;
else
return -1'));
for (int i = 0; i< 5; i++)
printf ("%s ", str_array[i]);
printf ("\n");
return 0;
}
不知道是不是真的有人愿意看……
本文中所用的 M4 知识,大部分出自[3]。每当我觉得 M4 知识匮乏的时候,就去翻翻[4]。其实,M4 非常简单,而且我用 M4 所实现的这点东西,用其他脚本也很容易实现,但是我觉得 M4 的实现非常简洁——简而不洁。以下代码是 c-lambda.m4 的全部内容:
divert(-1)
define(`C_HEAD', `divert(0)')
define(`C_LAMBDA', `divert(1)')
define(`C_BODY', `divert(2)')
define(`_set', `undefine(`$1')define(`$1', `$2')')
_set(`N', 0)
define(`lambda_define', `static $2 lambda_`'N`('$1`)'{$3;}')
define(`lambda',
`_set(`N', incr(N))`'dnl
`'`'`'`'C_LAMBDA`'lambda_define(`$1',`$2',`$3')
`'`'`'`'C_BODY`'lambda_`'N`'')
C_HEAD`'dnl
一共 12 行代码。如果牺牲一点可读性,可以简化到 10 行以内。
需要注意,c-lambda.m4 中将 N
定义为一个宏,这是非常非常非常危险的做法,因为如果 C 代码中也有 N
这个变量,那么它就会被 m4 视为一个 M4 宏并被展开。可以在 c-lambda.m4 用一个复杂一些的名字来代替 N
,例如 NNNNNNNNNNNNNNNNN
:D
最后需要强调一点,这个 lambda 并非闭包,也无法实现闭包,因为无法逾越 C 标准。如果不借助编译器的扩展,一个 C 函数要访问位于它外部的变量,唯一的办法是将外部的变量作为参数传给这个 C 函数。
参考文档
[1]
Vala 与 C 相映成趣
[2]
C语言的奇技淫巧
[3]
Notes on the M4 Macro Language
[4]
GNU M4 手册