前言
一直觉得函数式编程中的闭包和延迟计算是很神奇的技术,因为一直不知道原理,所以也不知道如何用好他们。看过几遍介绍,但终究是没有摸到什么头脑,直到一个偶然的机会,突然明白了...
一个延迟计算的例子
List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().map(s->s.toUpperCase()).peek(System.out::println).collect(Collectors.toList());
这是一个Java8中运用stream计算的一个例子,意思是把stringList中的所有字符串转换成大写的,然后输出出来,然后放到新的List中
有意思的是,如果代码写成这样
List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().map(s->s.toUpperCase()).peek(System.out::println);
它是不会进行System.out.println()操作的。而如果写成这样
List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
Stream<String> stream= stringList.stream().map(s->s.toUpperCase());
得到的stream里的字符串流还是小写的,这就是所谓的延迟计算。
其实我在这里挺讨厌延迟计算的,之前很不明白为什么不能直截了当的给我计算结果,而需要进行终结操作,事实上终结操作并不是我想要的,只是为了应对延迟计算不得已做的操作。这个问题先留在这,下面我们先看下闭包,因为这两个技术的原理是都来自高阶函数。
闭包
Java闭包的用法
public class FirstLambdaExpression {
public String variable = "Class Level Variable";
public static void main(String[] arg) {
new FirstLambdaExpression().lambdaExpression();
}
public void lambdaExpression(){
String variable = "Method Local Variable";
String nonFinalVariable = "This is non final variable";
new Thread (() -> {
//Below line gives compilation error
//String variable = "Run Method Variable"
System.out.println("->" + variable);
System.out.println("->" + this.variable);
}).start();
}
}
这是java8中的一个闭包的例子,用这个例子的主要目的就是演示下Java也可以用闭包,为什么使用闭包,一言以蔽之,就是为了在链式计算中维持一个上下文,同时进行变量隔离,这么说有点抽象,再举个例子
List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
stringList.stream().reduce((s1,s2)->s1+s2).get();
输出: <code>abccdeefgghiijk</code>
reduce()接收有两个参数的函数,它的作用是把上一次计算的结果作为第一个参数,然后把这次要计算的量作为第二个参数,然后进行计算。
如果不使用闭包呢,我们将会得到下面的代码
List<String> stringList=Arrays.asList("abc","cde","efg","ghi","ijk");
//System.out.println( stringList.stream().reduce((s1,s2)->s1+s2).get());
String temp="";
for(String s: stringList){
temp+=s;
}
System.out.println(temp);
我们需要一个中间变量来维持这个计算能进行下去。好吧,我承认这没有什么不可以接受的,我们之前就一直这样写。但是如果变成这样了呢
List<String> stringList1=Arrays.asList("abc","cde","efg","ghi","ijk");
List<String> stringList2=Arrays.asList("abc","cde","efg","ghi","ijk");
//System.out.println( stringList.stream().reduce((s1,s2)->s1+s2).get());
String temp="";
for(String s: stringList1){
temp+=s;
}
String temp1="";
for(String s: stringList2){
temp+=s;
}
System.out.println(temp);
System.out.println(temp1);
对应是使用闭包的写法
List<String> stringList1=Arrays.asList("abc","cde","efg","ghi","ijk");
List<String> stringList2=Arrays.asList("abc","cde","efg","ghi","ijk");
System.out.println( stringList1.stream().reduce((s1,s2)->s1+s2).get());
System.out.println( stringList2.stream().reduce((s1,s2)->s1+s2).get());
从这个例子中我们看到了使用中间变量的不便性,对java来说这个中间变量一般在方法里面,不会有多大影响,但是对应javascript来说,太容易造成变量污染了,尤其是你用完这个字符串忘掉置空或者使用前忘记置空了,这就是为什么闭包的特性在javascript中是与生俱来的,而在java中直到第八个版本才出现的原因了(开玩笑的。JavaScript是从一开始就是一种可以进行函数式编程的语言,java第八版本才开始变得可以进行函数式编程,闭包是函数式编程语言必须提供的一种特性,正如例子中的reduce()函数一样,能够接收函数作为参数的语言,必然也天生的实现了闭包)。
好了到目前为止我们已经对闭包和延迟计算有了一点点了解,那接下来我们就要探究下其实现原理了。在这我们先介绍一个概念高阶函数
高阶函数
定义
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
在数学中它们也叫做算子(运算符)或泛函。微积分中的导数就是常见的例子,因为它映射一个函数到另一个函数。
在无类型 lambda 演算,所有函数都是高阶的;在有类型 lambda 演算(大多数函数式编程语言都从中演化而来)中,高阶函数一般是那些函数型别包含多于一个箭头的函数。在函数式编程中,返回另一个函数的高阶函数被称为Curry化的函数。
在很多函数式编程语言中能找到的 map 函数是高阶函数的一个例子。它接受一个函数 f 作为参数,并返回接受一个列表并应用 f 到它的每个元素的一个函数。
范例
这是一个javascript 的例子, 其中函式 g() 有一引数以及回传一函数. 这个例子会打印 100 ( g(f,7)= (7+3)×(7+3) ).
function f(x){
return x + 3
}
function g(a, x){
return a(x) * a(x)
}
console.log(g(f, 7))
这是接收一个函数作为参数的例子,下面我们以一个返回一个函数的例子
function outer(){
var a=1;
var inner= function(){
return a++;
}
return inner
}
var b=outer();
console.log(b());
console.log(b());
分析
我们从javascript语言入手进行分析是因为java语言没有办法定义高阶函数,高阶函数是延迟计算和闭包的来源。顺便提一句,高阶函数的设计原理也并没有多么复杂,以我了理解,高阶函数实现起来大概来源于C语言的指向函数的指针,指向函数的指针也来源于汇编语言,对于这么底层的语言来讲,没有函数的概念只有代码块的概念,在代码块间跳来跳去,就实现了函数,在这里我就不展开来说了。
延迟计算
我们拿上面的返回函数的例子来讲
var b=outer();
此时,b是个什么?b是一个函数,此时
var b=function(){
return a++;
}
在这里<b>b只是函数定义,并没有执行,而函数执行的地方在于 <key>console.log(b());</key></b>
只用这一句话,就说明了 <em>延迟计算</em> 的实质,只定义不使用。
所以回头来看下我们前面的Java代码里“延迟计算”,这里就比较明了了,map函数和reduce函数只是接收了函数,并没有立即执行,这就是为什么需要一步终结操作了。
闭包
提到闭包不得不提另一个口号,那就是“在函数式编程中,函数是编程语言中的一等公民”,每个函数都可以当做对象来使用,再举一个例子
function a(){
var i=1;
return function () {
return ++i;
}
}
var b=a();
console.log(b());//2
var c=a();
console.log(b());//3
console.log(c());//2
可以看出b和c是隔离开的,互相不影响的,这里我们可以类比成Java中的代码:
class Outter{
int i=1;
public int inner(){
return this.i++;
}
}
Outter b=new Outer();
Outter c=new Outer();
System.out.println(b.inner());
System.out.println(b.inner());
System.out.println(c.inner());
在javascript中的写法也可以写成:
function Outter(){
var i=1;
var inner=function(){
return i++;
}
return inner;
}
var b=new Outter();//实际上返回一个inner对象
var c=new Outter();//实际上又返回一个inner对象
console.log(b());//1
console.log(b());//2
console.log(c());//1
console.log(c());//2
ok,到这里,基本上就能理解闭包如何使用了,在我看来,闭包实际上是函数式编程的面向对象编程,或者函数式编程中面向对象的一种实现方式。反正我是这么理解了闭包的,自从这样想明白之后,我突然变得会使用闭包了
以上