目前为止关于函数式编程各种功能的讨论都只局限在“纯”函数式语言范围内:这些语言都是lambda演算的实现并且都没有那些和阿隆佐形式系统相冲突的特性。然而,很多函数式语言的特性哪怕是在lambda演算框架之外都是很有用的。确实,如果一个公理系统的实现可以用数学思维来看待程序,那么这个实现还是很有用的,但这样的实现却不一定可以付诸实践。很多现实中的语言都选择吸收函数式编程的一些元素,却又不完全受限于函数式教条的束缚。很多这样的语言(比如Common Lisp)都不要求所有的变量必须为final,可以修改他们的值。也不要求函数只能依赖于它们的参数,而是可以读写函数外部的状态。同时这些语言又包含了FP的特性,如高阶函数。与在lambda演算限制下将函数作为参数传递不同,在指令式语言中要做到同样的事情需要支持一个有趣的特性,人们常把它称为lexical closure。还是来看看例子。要注意的是,这个例子中变量不是final,而且函数也可以读写其外部的变量:

  1. Function makePowerFn(int power) {
  2. int powerFn(int base) {
  3. return pow(base, power);
  4. }
  5.  
  6. return powerFn;
  7. }
  8.  
  9. Function square = makePowerFn(2);
  10. square(3); // returns 9

makePowerFn函数返回另一个函数,这个新的函数需要一个整数参数然后返回它的平方值。执行square(3)的时候具体发生了什么事呢?变量power并不在powerFn的域内,因为makePowerFn早就运行结束返回了,所以它的栈也已经不存在了。那么square又是怎么正常工作的呢?这个时候需要语言通过某种方式支持继续存储power的值,以便square后面继续使用。那么如果再定义一个函数,cube,用来计算立方,又应该怎么做呢?那么运行中的程序就必须存储两份power的值,提供给makePowerFn生成的两个函数分别使用。这种保存变量值的方法就叫做closure。closure不仅仅保存宿主函数的参数值,还可以用在下例的用法中:

  1. Function makeIncrementer() {
  2. int n = 0;
  3.  
  4. int increment() {
  5. return ++n;
  6. }
  7. }
  8.  
  9. Function inc1 = makeIncrementer();
  10. Function inc2 = makeIncrementer();
  11.  
  12. inc1(); // returns 1;
  13. inc1(); // returns 2;
  14. inc1(); // returns 3;
  15. inc2(); // returns 1;
  16. inc2(); // returns 2;
  17. inc2(); // returns 3;

运行中的程序负责存储n的值,以便incrementer稍后可以访问它。与此同时,程序还会保存多份n的拷贝,虽然这些值应该在makeIncrementer返回后就消失,但在这个情况下却继续保留下来给每一个incrementer对象使用。这样的代码编译之后会是什么样子?closure幕后的真正工作机理又是什么?这次运气不错,我们有一个后台通行证,可以一窥究竟。

一点小常识往往可以帮大忙。乍一看这些本地变量已经不再受限于基本的域限制并拥有无限的生命周期了。于是可以得出一个很明显的结论:它们已经不是存在栈上,而是堆上了8。这么说来closure的实现和前面讨论过的函数差不多,只不过closure多了一个额外的引用指向其外部的变量而已:

  1. class some_function_t {
  2. SymbolTable parentScope;
  3.  
  4. // ...
  5. }

当closure需要访问不在它本地域的变量时,就可以通过这个引用到更外一层的父域中寻找该变量。谜底揭开了!closure将函数编程与面向对象的方法结合了起来。下一次为了保存并传递某些状态而创建类的时候,想想closure。它能在运行时从相应的域中获得变量,从而可以把该变量当初“成员变量”来访问,也因为这样,就不再需要去创建一个成员变量了。