前言
一直覺得函數式編程中的閉包和延遲計算是很神奇的技術,因為一直不知道原理,所以也不知道如何用好他們??催^幾遍介紹,但終究是沒有摸到什么頭腦,直到一個偶然的機會,突然明白了...
一個延遲計算的例子
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,到這里,基本上就能理解閉包如何使用了,在我看來,閉包實際上是函數式編程的面向對象編程,或者函數式編程中面向對象的一種實現方式。反正我是這么理解了閉包的,自從這樣想明白之后,我突然變得會使用閉包了
?? 以上