七、function as first class
当设计一门语言时,函数部分设计五花八门,各种设计所对应的实现方式也各有千秋。
函数式编程
函数式编程 是一种编程范式(programming paradigm),也就是如何写程序的方法论。它将计算机运算看作是数学中函数的计算,并且避免了状态以及变量的概念。在函数式编程语言中,函数跟其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。且函数中没有副作用(函数内部与外部互动,如内部修改全局变量值),因此函数只返回新的值,不修改系统变量。在输入参数相同的情况下,得到的结果总是相同的。
C语言中的函数
C语言中的函数并不是严格意义上的函数,它具有副作用。这种函数的实现方式十分简单,对于函数体部分,生成相关代码,保存在静态区域即可。对于函数调用部分,可以将参数压入栈,然后调用。
比如 printf(1, 2, 2);
可以翻译成如下代码:
push 2
push 2
push 1
call printf
高阶函数
我们把接受一个或多个函数作为参数,或者能返回函数的函数叫做高阶函数。在C语言中可以通过函数指针进行传递函数:
typedef void(*func)();
void test() {}
void call(func f) {
f();
}
这样是将函数地址传递给调用函数,可以用汇编简要解释:
push offset _test
call _call
; in call
call [ebp-4]
嵌套函数
pascal 之类的语言就支持嵌套函数,所谓嵌套,就是可以在函数内部定义函数。C语言并不支持嵌套函数,我们假设其支持嵌套函数:
void global_func() {
int a = 20;
void nest_func() {
a = 10;
}
void nest_func1() {
int a = 40;
void test() {
nest_func();
a = 30;
}
a == 40;
test();
a == 30;
}
nest_func();
a == 10;
a = 20;
nest_func1();
a == 10;
}
nest_func(); /* error */
可以看到的是,嵌套定义函数可以访问外部作用域,并产生副作用。不同作用域相互之间屏蔽。嵌套作用域给函数实现带来了不少麻烦,比如在查找变量所在作用域时,需要沿着栈帧回溯。如上诉例子,test
在查找变量 a
时,应该找到 nest_func1
中的变量 a
,而 test
中调用的 nest_func
所查找的 a
却应该是 global_func
中的 a
。(这里假定使用的是 词法作用域, 而不是 动态作用域)这样就不能直接使用原有的向上回溯查找变量的方法,不过这有相应的解决办法。对于每个函数的帧,我们加入一个访问链指针,其指向当前调用栈中,该函数定义所在的外层函数最近一次调用的帧。比如对于上面的例子,在调用 nest_func1()
中的 test()
时,test
中的指针应当指向其直接上层 nest_func1
, 但是对于 test()
调用中的函数 nest_func
,其上层不应当是 test()
。
top -> nest_func()
test()
nest_func1()
global_func()
bottom ->
简单的函数调用栈
这里 nest_func
访问链应当指向最上面的 global_func()
。这样,函数在查找变量时,就可以通过访问链回溯而不是调用栈回溯。关于访问链的具体实现,可以参考龙紫书。还需要注意的是如果嵌套函数中有引用外层变量,那么是无法将内层函数当作返回值返回,所以这里需要引入闭包的概念。
柯里化函数
在闭包之前需要了解柯里化函数(Currying),简单来说柯里化函数就是函数生成的函数,比如以下C++代码:
int func(int a, int b) {
return a + b;
}
auto call = std::bind(func, 1, _1);
call(1);
在这里我们传递给函数 func
一个参数,然后生成一个保存了当前状态的函数,然后再后续补完全部参数时调用,这就是一个简单的柯里化函数应用。现在来考虑如何实现柯里化函数,柯里化函数需要保存当前已有的状态,那么我们对每一个函数包括普通函数定义一个 frame ,在每一次调用时,检测参数数目,如果满足所有参数都有对应的实参,那么就调用,否则生成一个新函数,将原有的 frame 拷贝一份,并加入新的参数。
if (args.size() == params.size())
call function;
else
insert new function
copy frame to new frame and insert params
闭包
闭包是指可以包含自由(未绑定到特定对象)变量的代码块,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义。可以把闭包当作嵌套的高阶柯里化函数,嵌套对应着代码块或全局上下文,柯里化函数则是保存当前上下文状态,高阶使得函数可以通过参数或返回值进行传递。对于闭包有多种实现方式,这里紧紧讨论一种非常 native 的实现方式。首先需要对闭包中访问的自由变量进行捕获,然后隐式地作用参数传递给闭包体,生成新函数返回。这样变换以后,新生成的函数并不依赖于其父作用域,使得函数可以在任意地方调用。
(define (func x)
(lambda (a) (+ a x))
可以改为
(define (func x)
((define (nest_func x a)
(+ a x)) x))