函数式编程的另类指南(11)

By | December 24, 2013

The following part is not maintained anymore. Please go to 函数式程序设计的另类指南 for the whole translation.

以下内容不再更新,浏览全部翻译,请访问 函数式程序设计的另类指南


原文链接:Functional Programming For The Rest of Us
原文作者:Vyacheslav Akhmechet

闭包(Closure)

我们已经讨论了纯函数式语言的很多功能——所谓“纯”函数式语言就是实现了lambda演算并且不包含与Church范式矛盾的特性。但是函数式语言并不仅限于lambda演算。虽然实现一个自我证明的系统非常有用,它可以让我们以数学的方式来思考程序,但是可能在实际中它没有什么用处。所以很多语言选择支持部分函数式元素,但又不严格遵守那些教条。一些语言(比如Common Lisp)不要求变量是final的。你可以随时修改变量。它们的函数也不仅依赖于函数的参数。函数可以访问外部状态。但这些语言的确包含了函数式特性,比如高阶函数。在非纯粹的函数式语言里以函数作为参数传递,和在lambda演算系统中有些不同,它需要一种被称为词法闭包(lexical closure)的有趣特性。让我们来看看这段例子代码。记住,这回变量不是final的,并且函数可以引用其作用域外的变量:

Function makePowerFn(int power) {
  int powerFn(int base) {
    return pow(base, power);
  }
  return powerFn;
}

Function square = makePowerFn(2);
square(3); // returns 9

函数makePowerFn返回了一个函数。这个函数有一个参数,并对这个参数进行幂运算。如果我们对square(3)求值会发生什么?变量power其实已经不在powerFn的作用域中了,因为makePowerFn已经返回,所以它的栈已经消失了。那么square是怎么执行的?一定是这个语言以某种方式将power的值保存起来以便square使用。如果我们创建另一个函数cube,为参数的立方运算会怎么样?运行环境必须存储两个power,每个我们用makePowerFn生成的函数(square和cube)各使用一个。存储这些值的现象就叫做闭包。闭包不只保存宿主函数的参数。例如,closure可能会是这样:

Function makeIncrementer() {
  int n = 0;

  int increment() {
    return ++n;
  }
}

Function inc1 = makeIncrementer();
Function inc2 = makeIncrementer();

inc1(); // returns 1;
inc1(); // returns 2;
inc1(); // returns 3;
inc2(); // returns 1;
inc2(); // returns 2;
inc2(); // returns 3;

运行时已经保存了n,所以递增器可以访问它。更重要的是,运行时为每个递增器都保存了一个n的拷贝,即使这些拷贝应该在makeIncrementer返回时消失。那么这些代码是被如何编译的?闭包运算又是怎么工作的?让我们去幕后看看。

一点常识会很有帮助,首先局部变量不再由简单的作用域规则限定,它们的生命周期也不确定。那么由此可以得出它们不再被保存在栈上,而是堆中。这样一来,闭包的实现就与我们前面讨论的函数一样了,只是它还有一个指向周围变量的引用。

class some_function_t {
  SymbolTable parentScope;

  // ...
}

当闭包引用了一个不在其作用域的变量时,它便会查找其父作用域中的引用。这样闭包将函数式和面向对象程序设计相结合。每当你创建一个包含了状态的类,并且把它传到别处的时候,想想闭包吧。闭包就是一个可以在运行时创建并获取“成员变量”的对象,只是你不必亲自去做这些!

Leave a Reply

Your email address will not be published. Required fields are marked *