解释函数式编程中的闭包概念,非完全翻译自 wiki: closure
文章中使用英文名词,以防止翻译不当和歧义
原文: wiki: closure
译文: 闭包的概念
译者: Breaker <breaker.zy_AT_gmail>
译文
closure 亦称词法闭包 (lexical closure)、函数闭包 (function closure) 或函数值 (function value),是由 一个函数 和 这个函数的 non-local 变量的引用环境 (referencing environment) 组成的实体。这里的 non-local 变量和 C++ 中的 static 和全局变量是不同概念,见 wiki: non-local variable
第一级函数 (first-class function): 函数如同整数和字符串等基本类型一样,作为参数传递、返回值 和 绑定变量名
状态表达 (state representation): closure 在一个函数和一组私有 upvalue 变量之间建立关联关系,多次调用中保持 upvalue 值,并且只能从 closure 函数内访问 upvalue,这些可用在状态式语言中的状态表达范型和信息隐藏
控制结构: 闭包只在被调用时才执行函数,即延迟求值 (delay evaluation),可用来定义控制结构,如 Smalltalk 中的 if 条件分支和 while 循环控制结构都通过 closure 实现
多个 closure 函数用共享环境进行消息传递
实现对象系统: 见 Re: FP, OO and relations. Does anyone trump the others?
引用环境绑定在 closure 上的自由变量 (free variable) 或 non-local 名字称为 upvalue,upvalue 的生存期可持续到 closure 的生存期结束
当执行进入 closure 函数时,函数可以访问 closure 绑定的 upvalue,只要 closure 不释放,则调用之间保持 upvalue 的值
在一些语言中,在函数中定义另一个函数时,如果内部函数引用了外部函数的局部变量,则可能产生 closure。运行时,一旦外部函数执行,就会形成 closure,其包含了内部函数的代码 和 其到外部函数所需变量的引用 (upvalue)
在大多数支持 closure 的语言中,经常用匿名函数来构造 closure,但它们是不同的两个概念
可以认为匿名函数是函数字面量 (function literal),偏向右值和函数体的概念;而 closure 是函数变量 (function value),偏向左值和对象的概念
closure 会保持引用环境,如 upvalue 的当前值,而匿名函数不必如此
closure 术语最早由 Peter J. Landin 在其 SECD machine 中定义,用以表达式求值中的 environment part 和 control part (1964)
后来 Joel Moses 用 closure 指代 lambda 表达式 其绑定开放的 free variable 被词法环境 (lexical environment) 关闭绑定 (bound in) 时,形成的关闭表达式 (closed expression) 即 closure
后来 Sussman 和 Steele 采用了 closure 概念,并首次在 Scheme 中实现 (1975),以支持词法作用域 (lexically-scoped) 的 first-class function
上面的 lexical environment, lexically-scoped 可以非正式的理解为语言中的作用域
closure 最显著的使用是 ML 和 Lisp 等语言的函数式编程 (functional programming)
传统的命令式语言 (imperative language, e.g. Algol, C, Pascal) 不支持 closure,因为它们不支持 non-local 名字,只有在嵌套函数和匿名函数中才引入此概念,同时它们也不支持 higher-order 函数
现代的带垃圾收集 (GC, garbage collection) 的命令式语言 (Smalltalk, C#) 和 解释性的脚本语言,很多都支持 higher-order 函数和 closure
closure 典型的实现方法,采用包含下面两者的一个数据结构:
指向函数代码的指针
函数 lexical environment 的表示结构,如 closure 创建时,函数的可用变量集合及其值
如果语言采用在堆栈上分配局部变量的运行时内存模型,那么很难实现完全的 closure,因为函数返回时会释放局部变量
closure 需要 upvalue 在 enclosing function 即外层函数执行结束后继续保持,因此 upvalue 应该一直保持的内存分配,直到最终不需要时才释放,所以很多支持 closure 的语言都使用 GC
另外,语言也可以选择接受特定使用导致的不确定行为,如 C++ 标准的 lambda 表达式实现提议,见 Lambda Expressions and Closures
Funarg problem (functional argument problem) 说明了在基于堆栈的语言,如 C/C++ 中实现 first class function 的困难
第一版 D 语言,假定程序员知道如何对待从定义作用域中返回后引用无效的 delegate 和局部变量(局部变量在堆栈上分配),此时仍能使用很多 functional pattern,但对于复杂情况需要显式地对变量进行堆分配
第二版 D 解决了这个问题,它检测哪些变量必须存储在堆上,并且执行自动分配。因为 D 的两个版本都使用 GC,所以不需要跟踪传递时变量的使用
在一些使用 immutable data 的严格函数式语言,如 Erlang 中,很容易实现自动内存管理 (GC),因为它们没有变量引用。如在 Erlang 中,所有的变量都在堆上分配,但是它们的引用存储在堆栈上,当函数返回后,引用依然有效,堆清理由增量 GC 完成
ML 中局部变量在堆栈上分配,当创建 closure 时,会将 closure 所需的变量值拷贝进 closure 的数据结构中
Scheme 是类似 ALGOL 的词法作用域系统,带有动态变量和 GC,缺少堆栈编程模型,所以也没有基于堆栈语言的限制,可以直接表达 closure。包含执行代码和环境 free variable 的 lambda 表达式,可在程序中一直保持访问,也可被其它 Scheme 表达式使用
closure 和并发计算 Actor model 中的 Actor 有紧密关系,那里函数 lexical environment 中的值称为 acquaintance。关于 closure 在 并发编程 语言中的一个重要问题是:closure 中的变量是否可更新,如何同步这些更新,Actor 给出一种方案,见 Foundations of Actor Semantics
上面的 closure wiki 解释比较抽象,下面是一个具体的 Javascript closure 示例:
说明:
[END]