1. 程式人生 > >Java FP: Java中函數語言程式設計的Map和Fold(Reduce)

Java FP: Java中函數語言程式設計的Map和Fold(Reduce)

原文連結 作者:  Cyrille Martraire  譯者: 李璟([email protected])

在函數語言程式設計中,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操作是高度並行的,所以可以將其用於做大量的並行運算。

參考文獻