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步稍顯不同:
- 建立一個輸出列表。
- 為輸入列表中每個元素呼叫某個給定的函式,將函式結果存入輸出列表中。
- 返回輸出列表。
你經常使用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操作是高度並行的,所以可以將其用於做大量的並行運算。