匿名函数与闭包实现
简单记录一下如何实现函数式语言的必备功能——匿名函数与闭包。
closure 闭包
看一段 Python 中闭包的例子:
1 | def add2(a): |
Python 是允许函数嵌套的,其中 add1
就是一个闭包的例子。闭包与函数的区别在于,闭包中还包含了在那个函数中出现的自由变量(由函数外部决定取值的变量)的值。在 add1
中,a
没有在它的参数列表中,也没有在函数体内被定义,而是来源于上层函数 add2
的参数,那么它就是一个自由变量。也就是说,add1
作为一个闭包,它同时拥有函数定义以及自由变量 a
的取值。
1 | add2(1) = closure(<function definition>, a=1) |
再来看一下 C++ 中的例子:
1 | std::function<int(int)> add2(int a) { |
一个微小的区别就是,我手动指定了闭包中需要捕获的自由变量是 a
(当然也可以用 &
或者 =
来自动按引用或者按值捕获)。
lambda lifting 匿名函数提升
假设我们的目标代码是 machine code(机器语言),汇编中可以有函数的概念(比如对于 x64,参数按顺序放在寄存器里,返回值放在 eax
里,还有 call
指令),但肯定没有闭包的概念(因为连变量的概念也没有)。那么我们要做的事情就是把闭包变得和普通函数一样。
同样以 python 为例:
1 | class Closure: |
和之前的区别在于,add1
不再是一个嵌套函数,而是变成了一个顶层函数(这个过程就被称为 lambda lifting)。对于闭包函数中的自由变量,塞进了一个名为 __env
的函数参数里。而闭包本身不再是一个 <function>
的对象,而是一个 Closure
类的对象,包含了函数本身以及所需的自由变量的值。
lambda function 匿名函数
剩下的就是处理匿名函数的问题了,这部分其实最为简单。依旧以 Python 为例子:
1 | factorial = lambda x: x * factorial(x - 1) if x > 0 else 1 |
唯一的区别就是给 lambda 取个名字,然后用之前提到的方法把它变成顶层函数,再用对应的 closure 替换原来这个 lambda 所在的位置。
实现
为了实现 lambda 和 closure,编译器需要做的事情有:
- 给匿名函数取个名字
- 分析所有函数的自由变量,并用一个新的顶层函数替代它,这个新的顶层函数参数除了原有参数外还包含了所需的自由变量
- 用一个包含函数指针 (f) 和所有自由变量取值 (env) 的结构体 (closure) 取代所有匿名函数或者闭包所出现的位置
最后效果如下:
1 | struct closure { |
注:
- 为了统一,所有的函数以闭包形式出现,对于原有的顶层函数或者不含自由变量的匿名函数,env 参数为空)
- 虽然以 C++ 代码呈现,但是可以很容易地转成 LLVM IR
参考
- https://nim-lang.org/docs/intern.html#code-generation-for-closures