閱讀499 返回首頁    go 技術社區[雲棲]


Java FP: Java中函數式編程的Map和Fold(Reduce)

在函數式編程中,Map和Fold是兩個非常有用的操作,它們存在於每一個函數式編程語言中。既然Map和Fold操作如此強大和重要,但是Java語言缺乏Map和Fold機製,那麼該如何解釋我們使用Java完成日常編碼工作呢?實際上你已經在Java中利用手動編寫循環的方式實現了Map和Fold操作(譯者注:許多動態語言如python都提供了內置的實現)。

免責聲明:本篇文章僅僅隻是一篇入門簡介,並非函數式編程的參考。函數式編程愛好者可能會不讚同本文觀點。

你已經很熟悉Map和Fold

假設這裏有一個List<Double>,存儲了不含增值稅VAT(譯者注:Value Added Tax)的金額列表,現在我們想把這個列表轉換成包含增值稅金額的列表。首先我們定義一個方法,為金額添加增值稅:

public double addVAT(double amount, double rate) {
    return amount * (1 + rate);
}

現在將這個方法應用到每份金額上:

public List<Double> addVAT(List<Double> amounts, double rate) {
    final List<Double> amountsWithVAT = new ArrayList<Double>();
    for(double amount : amounts) {
        amountsWithVAT.add(addVAT(amount, rate));
    }
    return amountsWithVAT;
}

我們創建了一個輸出列表,它的大小與輸入列表一致,存儲了對輸入列表中每個元素應用了addVAT()之後的結果。恭喜你,我們剛才手工完成了對輸入列表應用addVAT()的Map操作。讓我們再來一次。
現在我們想利用匯率把每一份金額轉換成另一種貨幣的金額,所以我們需要一個新的函數:

public List<Double> convertCurrency(List<Double> amounts, double currencyRate) {
    final List<Double> amountsInCurrency = new ArrayList<Double>();
    for(double amount : amounts) {
        amountsInCurrency.add(convertCurrency(amount, currencyRate));
    }
    return amountsInCurrency;
}

請注意,這兩個方法接收同樣的列表,除了在以下第2步稍顯不同:

  1. 創建一個輸出列表。
  2. 為輸入列表中每個元素調用某個給定的函數,將函數結果存入輸出列表中。
  3. 返回輸出列表。

你經常使用Java完成上述的工作,這正式一個標準的Map操作:對輸入列表list<T>中的每個元素應用給定的函數someMethod(T),返回一個同樣大小的Map結果列表list<T>。

函數式編程語言意識到這樣特殊的需求(為集合中每個元素應用某個方法)是非常常見的,所以設計者把這種行為封裝到了內建函數Map中。這意味著,對於給定的addVAT(double, double) 方法,我們可以直接利用Map操作寫出這樣的代碼:

List amountsWithVAT = map (addVAT, amounts, rate);

是的,第一個參數是一個函數。因為在函數式編程語言中,函數是第一要素,所以函數可以被當做是參數傳遞給方法。

代碼中使用了Map操作,將會比使用了循環更加清晰以及更加不容易出錯,並且代碼的意圖會更加明確,但是Map操作並不存在於Java中。

以上例子的重點是,你已經很熟悉你甚至不知道的函數式編程關鍵概念:Map操作。

現在輪到Fold操作

回到之前提到的包含了金額的列表中,現在我們需要計算列表中每個金額之和。很簡單,我們用循環實現:

public double totalAmount(List<Double> amounts) {
    double sum = 0;
    for(double amount : amounts) {
        sum += amount;
    }
    return sum;
}

基本上我們將了“+=”函數,應用到列表中每一個數字元素上,遞增式地把每個元素並攏到一個元素裏,實現了一個Fold操作。Fold與Map類似,不同的是Fold返回一個標量而非一個列表。

同樣,這也是你經常用Java編寫的代碼,現在這段代碼擁有了在函數式編程語言中的名字:Fold或者Reduce。在函數式編程語言中,Fold操作通常是遞歸式的,這裏不進行深入討論。然而,我們可以在一個循環體內,利用可變狀態累加每次循環之後的結果,實現類似Fold的操作。在這種方式中,Fold操作將一個帶有內部可變變量並且讀取單個參數的函數,比如someMethod(T),應用到輸入列表list<T>中的每個元素中,一直到產生最後的Fold操作的結果之後結束。

典型的Fold操作如累加,邏輯與、邏輯或,List.add()和List.addAll(),StringBuilder.append(),max以及min等。

Fold的思想與SQL中的聚集函數類似。

在圖形中思考

可以利用草圖輔助我們思考。Map操作讀取一個長度為n的列表,並且返回一個處理過後的同樣大小的列表:

另一方麵,Fold操作讀取一個長度為n的列表,返回一個標量:

Eclipse模板

Map和Fold如此常用,我們在Eclipse中為這兩個操作創建模板,比如Map:

走進Java中的Map和Fold

Map和Fold是一種期望讀取到函數對象作為參數的代碼結構。在Java中,將待傳遞函數包裝到接口中,傳遞此接口的某個實現,是唯一的實現傳遞函數的途徑。

在Apache Commons Collections中,有兩個接口能滿足我們的需求:隻有transform(T):T方法的Transformer接口以及隻有execute(T):void方法的Closure接口。CollectionUtils為Java集合類提供了簡陋的類似Map的collect(Iterator, Tramformer)方法,以及一個利用Closure模擬Fold操作的的forAllDo()方法。

Google Guava的Iterables提供了一個靜態的Map操作方法transform(Iterable, Function)。

List<Double> exVat = Arrays.asList(new Double[] { 99., 127., 35. });
Iterable<Double> incVat = Iterables.transform(exVat, new Function<Double, Double>() {
    public Double apply(Double exVat) {
        return exVat * (1.196);
    }
});
System.out.println(incVat); //print [118.404, 151.892, 41.86]

類似的transform方法的實現同樣可以用在List和Map集合類中。

為了在Java中模擬Fold操作,可以使用Apache Common Collection中的Closure接口,該接口僅包含一個execute(T):void方法,所以你必須在內部維護當前可變狀態,就像“+=”操作那樣。

不幸的是,盡管被強烈要求,但是Guava中沒有類似Fold操作的實現,甚至連類似閉包的功能都沒有。但是實現你自己的Fold操作其實並不難,比如,你可以用以上提到的類簡單封裝:

// the closure interface with same input/output type
public interface Closure<T> {
    T execute(T value);
}
// an example of a concrete closure
public class SummingClosure implements Closure<Double> {
    private double sum = 0;
    public Double execute(Double amount) {
        sum += amount; // apply '+=' operator
        return sum; // return current accumulated value
    }
} 
// the poor man Fold operator
public final static <T> T foreach(Iterable<T> list, Closure<T> closure) {
    T result = null;
    for (T t : list) {
        result = closure.execute(t);
    }
    return result;}
@Test // example of use
public void testFold() throws Exception {
    SummingClosure closure = new SummingClosure();
    List<Double> exVat = Arrays.asList(new Double[] { 99., 127., 35. });
    Double result = foreach(exVat, closure);
    System.out.println(result);// print 261.0
}

並非隻為簡單集合:在樹形結構和其他結構上進行Fold

除了能操作簡單集合,還能應用於任何有向結構中,這是Map和Fold的強大之處。

想象一下,一個樹形結構將Node類作為它的子節點。把深度優先搜索DFS和廣度優先搜索BFS分別編寫到一個通用的接收Closure作為參數的方法中,會是一個非常不錯的主意:

public class Node ...{
    ...
    public void dfs(Closure closure){...}
    public void bfs(Closure closure){...}
}

我以前經常使用這樣的技巧,並且我發現利用一個通用的方法替代許多看起來相似的方法之後,可以大幅減少類的大小。最重要的是,可以通過偽造閉包實現遍曆的單元測試,每個閉包同時也可以獨立地進行單元測試。

訪問者模式同樣可以實現相似的功能,有可能你已經非常熟悉這個模式了。我不止一次在代碼中發現,訪問者模式非常適用於在遍曆數據結構期間對狀態的累加。在這個條件下,該訪問者就是一個Fold操作的傳遞給其他函數的特殊閉包Closure。

一句話描述Map-Ruduce

也許你已經聽過Map-Reduce模式。是的,Map和Reduce分別指的是我們提到過的Map和Fold的函數操作。雖然實際的應用程序非常複雜,但是不難理解,Map操作是高度並行的,所以可以將其用於做大量的並行運算。

參考文獻

Thinking functional programming with Map and Fold in your everyday Java

最後更新:2017-05-23 10:32:27

  上一篇:go  跟著實例學習ZooKeeper的用法: Barrier
  下一篇:go  跟著實例學習ZooKeeper的用法: 計數器