从λ演算到函数式编程聊闭包(1):闭包概念在Java/PHP/JS中形式

什么是闭包

如果让谷哥找一下“闭包”这个词,会发现网上关于闭包的文章已经不计其数

维基百科上对闭包的解释就很经典:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 Peter J. Landin 在1964年将术语闭包定义为一种包含环境成分和控制成分的实体。
百度百科:

闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。

闭包概念:

  闭包就是有权访问另一个函数作用域中变量的函数.

分析这句话:

  1.闭包是定义在函数中的函数.

  2.闭包能访问包含函数的变量.

  3.即使包含函数执行完了, 被闭包引用的变量也得不到释放.

在 Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c、swift 以及Java(Java8及以上)等语言中都能找到对闭包不同程度的支持。

抽象代数中的闭包

在离散数学(具体的说是抽象代数)里,如果对一个集合中的每个元素执行某个运算操作,得到的结果还是这个集合的元素,那么就说该集合在这个运算操作下构成闭包。例如,整数集合在减法运算下构成闭包;但是自然数在减法运算下不构成闭包。

封闭的定义

  有了集合和运算的概念,就可以定义封闭的概念了。
  非正式地,如果定义于集合A上的运算+的运算结果仍然属于A,那么运算+对于集合A是封闭的。下面给出“封闭”的一个半形式化定义:
  如果对于任意a1,a2∈A,有a1+a2∈A,那么说二元运算+对于集合A是封闭的。(⊙,+ 与”,“或”)
  例如“+”对于N+是封闭的,因为任意两个正整数的和结果仍然是正整数;但是“>”对于N+不是封闭的,例如3和5属于N+,但是:3>5=>2∉N+。

闭包性质

  一个集合满足闭包性质当且仅当这个集合在某个运算或某些运算的搜集下是封闭的,其中“某些运算的搜集下封闭”是指这个集合单独闭合在每个运算之下。

  值得一提的是,之前这条定义往往被作为一条公理引入一个代数结构,叫做“闭包公理”。但是现代集合论往往将运算形式化的定义为集合间的运算,所以将其作为公理引入代数结构是多余的(因为可以通过其它公理间接定义闭包公理),但是对于子集是否闭合的问题,闭包公理仍然有意义。

如此讲下去呢,哎,肯你说。我勒去…好高大上啊,完全看不懂!!!…………好吧。!!至此,打住……关于

函数式编程中的闭包

在这一章节开始之前,我需要再和大家明确一个比较纠结的事实,就是在函数式编程领域中当说到“闭包”时,也有可能是指数学领域中闭包的概念,这是因为函数式编程在基础理论与抽象代数有一定亲缘性,所以当在函数式语言著作中讨论“闭包”时,有可能是在抽象数学的上下文中讨论的。然而,在表述上可能会有微妙变化。在函数式语言领域对于数学闭包常用的表述是“如果一个运算的结果仍然能被此运算作用,则这个运算是封闭的”,要注意这只不过是上文提到的“闭包”概念的另一种等价表述而已,如果我们将这个运算的所有结果看做一个集合,那么就可以等价表述说这个运算在这个集合上是封闭的。

而我下面所要阐述的闭包是一种截然不同的概念。所以,当在函数式语言的著作中看到“闭包”时,需要根据上下文环境小心区分其表述哪种概念。

Lambda演算与自由变量

函数式编程语言的基础是lambda演算,这是一套用于研究函数定义、应用和递归的形式系统,由数学家丘奇在20世纪30年代引入。如果您不太熟悉lambda演算,那么维基百科相关页面是很好的快速入门资料,请原谅我不会完整描述lambda演算(因为已经有很多可以参考的资料)。

简单来说lambda演算将计算过程看过一系列的函数代换,例如,下面是加运算的lambda函数(假设+运算已经定义):λx.λx+y

lambda演算就是反复将函数应用于实际值,并用实际值代替参数,最终得出结果。例如计算7+2:(λx.λx+y)7 2=>(λy.7+y)2=>7+2=>9

首先用第一个参数(7)代替最外层函数的参数(x),然后用第二个参数(2)代替第二层函数的参数(y),最终得到计算结果。

鉴于如果下面大量使用lambda演算描述问题大家可能会崩溃(我也会崩溃),我将改用函数式语言scheme(Lisp的一个方言)来进行问题描述。注意其实scheme在本质上与lambda演算是等价的,只是看起来更好懂,例如不需要遵循lambda演算一个变态的规定:每个函数只允许有一个参数(虽然任何多参数函数式程序都可以通过Currying过程化归为等价的lambda演算)。

下面是用scheme程序对上述lambda演算的等价表示:(define (f x y) (+ x y))

可以这样计算7+2:(f 7 2);Value: 9

下面看一个稍微复杂点的例子:(define (f x) (lambda (y) (+ x y)))

这里定义了函数f,接受一个参数x,特别要注意它的返回值:不是一个值而是一个匿名函数。如果我们把这个函数单独拿出来:(lambda (y) (+ x y))

可以看到,这个匿名函数接收一个参数y,但是却没有参数x!也就是说,如果脱离上下文执行这个函数,则x处于未指定状态,我们说对于这个函数,y是绑定的,而x是自由的。

一般地:x是一个函数的函数体中的变量,如果x被这个函数的参数指定,则x是绑定于这个函数的,否则说x对于此函数是自由的。

下面可以看到,变量的绑定和自由概念是理解闭包本质的一把钥匙。关于这方面的内容,推荐阅读:《闭包漫谈(从抽象代数及函数式编程角度)

程序语言的闭包性质

继续上面的scheme程序,我们已经定义了函数f:(define (f x) (lambda (y) (+ x y)))

如果我们运行下面程序:(f 7);Value 13: #[compound-procedure 13]

可以看到,f返回了一个过程(匿名函数),按照函数演算规则,这个函数应该是:(lambda (y) (+ 7 y))

那么下面的运算就很直观了:((f 7) 2);Value: 9

注意这里有一个非常重要的地方(也是闭包性质的关键),那就是这个运算执行了两个函数:f和匿名函数。而f的作用域为(f 7),这就是说,其实在(f 7)之后,f这个函数就结束了,而x(这里被赋值为7)是f的私有变量(绑定于f),那么程序设计语言的设计者就有两种选择:

  • 第一,在函数超出其作用域后立即销毁其绑定变量,如果是这样的话,((f 7) 2) 是无法得出结果的,因为在外层的f运算结束后,存放数值“7”的变量就被释放了,所以匿名函数无法得到其自由变量x的值;

  • 第二,如果一个函数返回另一个函数,而被返回函数又需要外层函数的变量时,不会立即释放这个变量,而是允许被返回的函数引用这些变量。支持这种机制的语言称为支持闭包机制,而这个内部函数连同其自由变量就形成了一个闭包。

显然scheme的设计者做了第二种选择。

上面的这段话不太好理解,但是请务必多读几遍,因为,这就是闭包的全部。

在程序语言中,闭包就是一种语法糖,它以很自然的形式,把我们的目的和我们的目的所涉及的资源全给自动打包在一起,以某种自然、尽量不让人误解的方式让人来使用至于其具体实现,我个人意见,在不影响使用的情况下,不求甚解即可。在很多情况下,需要在一段代码里去访问外部的局部变量,不提供这种语法糖,需要写非常多的代码,有了闭包这个语法糖,就不用写这么多代码,自然而然的就用了。

java闭包,推荐阅读《深入理解Java闭包概念

简单理解:闭包能够将一个方法作为一个变量去存储,这个方法有能力去访问所在类的自由变量。

Java中闭包实现,关键点:

  • 如何用变量去存储方法?

  • java中能够保存方法的变量指的就是普通的对象

  • 如何让这个普通对象能够访问所在类的自由变量?

  • 纯天然的解决办法是:内部类。内部类能够访问外部类的所有属性及方法。

  • 隐藏具体实现是内部类的作用之一,如何保证隐藏具体实现的同时还能将闭包传递到外部使用?

让内部类实现通用接口,然后将内部类对象向上转型为接口类型。

上述解决办法就是Java最常用的闭包实现办法(内部类+接口)

 

public class Milk {  
    public final static String name = "纯牛奶";//名称  
    private static int num = 16;//数量  
    public Milk()  
    {  
        System.out.println(name+":16/每箱");  
    }   
    /** 
     * 闭包 
     * @return 返回一个喝牛奶的动作 
     */  
    public Active HaveMeals()  
    {  
        return new Active()  
                {  
                    public void drink()  
                    {  
                        if(num == 0)  
                        {  
                            System.out.println("木有了,都被你丫喝完了.");  
                            return;  
                        }  
                        num--;  
                        System.out.println("喝掉一瓶牛奶");  
                    }  
                };  
    }  
    /** 
     * 获取剩余数量 
     */  
    public void currentNum()  
    {  
        System.out.println(name+"剩余:"+num);  
    }  
}  
/** 
 * 通用接口 
 */  
interface Active  
{  
    void drink();  
}

php闭包

php中,闭包(Closure)又叫做匿名函数,也就是没有定义名字的函数。

//例一
//在函数里定义一个匿名函数,并且调用它
function printStr() {
    // 定义一个闭包,并把它赋给变量 $func 
    $func = function( $str ) {
        echo $str;
    };
    $func( 'some string' );
}
printStr();

//例二
//在函数中把匿名函数返回,并且调用它
function getPrintStrFunc() {
    $func = function( $str ) {
        echo $str;
    };
    return $func;
}
$printStrFunc = getPrintStrFunc();
$printStrFunc( 'some string' );

//例三
//把匿名函数当做参数传递,并且调用它
function callFunc( $func ) {
    $func( 'some string' );
}
$printStrFunc = function( $str ) {
    echo $str;
};
callFunc( $printStrFunc );
//也可以直接将匿名函数进行传递。如果你了解js,这种写法可能会很熟悉
callFunc( function( $str ) {
    echo $str;
} );

javascript闭包

function f(x){
    return function(y) {
        return x + y;
    };
}
var lam = f(7);
console.log(lam(2));

对于java,闭包用的真的不多,在JS,闭包是必考题。下篇讲解JS闭包:《从抽象代数漫游函数式编程(2):话说JavaScript闭包

 

参考文章:

闭包漫谈(从抽象代数及函数式编程角度)http://www.codinglabs.org/html/closure-perspective-of-abstract-mathematic-and-functional-language.html

Brendan Eich的自述 http://brendaneich.com/2008/04/popularity/

深入理解Java闭包概念 https://www.zhoulujun.cn/html/java/javaBase/7967.html

 

 

转载本站文章《从λ演算到函数式编程聊闭包(1):闭包概念在Java/PHP/JS中形式》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2015_0814_240.html

posted @ 2021-08-24 22:04  zhoulujun  阅读(210)  评论(0编辑  收藏  举报