免責聲明:我不是函數式編程的參考,本文不過是一個簡短的介紹; FP愛好者可能不太喜歡它。
您已經熟悉了
想象一下不含增值稅的數量的List <Double>。 我們希望將此列表轉換為包含增值稅的另一個相應列表。 首先,我們定義一種將增值稅加到一個單一金額中的方法:
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。 讓我們再做一次。
現在我們要使用貨幣匯率將每個金額轉換為另一種貨幣,因此我們需要一種新的方法:
公共double convertCurrency(double
public double convertCurrency(double amount, double currencyRate){return amount / currencyRate;}
現在,我們可以將此方法應用于列表中的每個元素:
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運算符:將給定方法someMethod (T):T應用于list <T>的每個元素,這將為您提供另一個相同大小的list <T>。
功能語言認識到這種特殊需求(在集合的每個元素上應用一種方法)非常普遍,因此他們將其直接封裝到內置的Map運算符中。 這樣,給定addVAT(double,double)方法,我們可以使用Map運算符直接編寫如下代碼:
List amountsWithVAT = map (addVAT, amounts, rate)
是的,第一個參數是一個函數,因為函數是函數語言中的一等公民,因此可以將它們作為參數傳遞。 與for循環相比,使用Map運算符更簡潔,更不易出錯,其意圖也更加明確,但是我們在Java中沒有它……
因此,這些示例的要點是,您已經不熟悉甚至不知道函數式編程的關鍵概念:Map運算符。
現在是Fold運算符
回到金額列表,現在我們需要將總金額計算為每個金額的總和。 超級容易,讓我們循環執行一下:
public double totalAmount(List<Double> amounts){double sum = 0;for(double amount : amounts){sum += amount;}return sum;
}
基本上,我們只是在列表上進行了折疊,使用函數“ + =”將每個元素折疊成一個元素,這里是一個遞增的數字,一次折疊一個。 這類似于Map運算符,除了結果不是列表而是單個元素(標量)。
這又是您通常用Java編寫的那種代碼,現在您已經用功能語言為其命名:“ Fold ”或“ Reduce”。 Fold運算符通常在函數式語言中是遞歸的,因此我們在此不再對其進行描述。 但是,我們可以使用某種可變狀態在迭代之間累加結果,從而以迭代形式實現相同的目的。 在這種方法中,Fold采用一種內部可變狀態的方法,該方法期望一個元素,例如someMethod(T),并將其反復應用于輸入列表<T>中的每個元素,直到我們得到一個單個元素T,即折疊操作的結果。
與Fold一起使用的典型函數是求和,邏輯與和或,List.add()或List.addAll(),StringBuilder.append(),max或min等。Fold的思維方式類似于SQL中的聚合函數 。
形狀思考
直觀地思考(帶有草率的圖片),Map接收一個大小為n的列表,并返回另一個大小相同的列表:

另一方面,Fold獲取大小為n的列表,并返回單個元素(標量):

您可能還記得我以前關于謂詞的文章 ,這些文章通常用于將集合過濾為元素較少的集合。 實際上,此過濾器運算符是在大多數功能語言中補充Map和Fold的第三種標準運算符。
Eclipse模板
由于Map和Fold很常見,因此有必要為它們創建Eclipse模板,例如Map:

在Java中更接近地圖和折疊
Map和Fold是期望函數作為參數的構造,而在Java中,傳遞方法的唯一方法是將其包裝到接口中。
在Apache Commons Collections中,有兩個接口對于我們的需求特別有趣: Transformer (具有一個方法transform(T):T )和Closure (具有一個方法execute(T):void) 。 類CollectionUtils提供了collect(Iterator,Transformer)方法(它基本上是Java集合的窮人Map運算符)以及forAllDo()方法,該方法可以使用閉包來模擬Fold運算符。
使用Google Guava, Iterables類提供了靜態方法transform(Iterable,Function) ,該方法基本上是Map運算符。
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]
類似的變換()方法,也可在類解釋為解釋和地圖為地圖。
要在Java中模擬Fold運算符,可以使用Closure接口,例如Apache Commons Collection中的Closure接口,僅使用一個帶有一個參數的單一方法,因此您必須在內部保留當前的-mutable-狀態,就像'+ =確實。
不幸的是,盡管Guava中沒有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 '+=' operatorreturn 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
}
不僅用于收藏:可折疊在樹木和其他建筑物上
Map and Fold的功能不僅限于簡單的集合,還可以擴展到任何可導航的結構,尤其是樹和圖。
想象一棵使用節點類及其子節點的樹。 將深度優先搜索和廣度優先搜索(DFS和BFS)編碼為兩個接受Closure作為單個參數的通用方法,可能是一個好主意:
public class Node ...{...public void dfs(Closure closure){...}public void bfs(Closure closure){...}
}
我過去經常使用這種技術,我可以說它可以大大減少類的大小,僅使用一種通用方法,而不是許多看起來相似的方法(每個方法都會重做自己的樹遍歷)。 更重要的是,可以使用模擬閉包對遍歷本身進行單元測試。 每個封蓋也可以獨立進行單元測試,所有這些都使您的生活變得更加簡單。
訪客模式可以實現非常相似的想法,您可能已經很熟悉。 我在我的代碼和其他幾個團隊的代碼中多次看到,Visitor非常適合在遍歷數據結構期間累積狀態。 在這種情況下,Visitor只是閉合中傳遞給折疊的一種特殊情況。
Map-Reduce上的一個字
您可能聽說過Map-Reduce模式,是的,其中的“ Map”和“ Reduce”一詞指的是我們剛剛看到的相同的函數運算符Map和Fold(也稱為Reduce)。 即使實際應用更加復雜,也很容易注意到Map 令人尷尬地是并行的,這對并行計算有很大幫助。
參考: 與我們的JCG合作伙伴一起 在您的日常Java中使用Map和Fold進行函數式編程的思考 ? Cyrille Martraire 博客上的Cyrille Martraire 。
翻譯自: https://www.javacodegeeks.com/2012/03/functional-programming-with-map-and.html