Java函數語言程式設計中歸約reduce()的使用教程

banq發表於2021-05-25

歸約Reduce流運算允許我們透過對序列中的元素重複應用合併操作,從而從元素序列中產生一個單一結果。其中參與者有三者:
  • 標識identity:代表一個元素,它是歸約reduce運算的初始值,如果流為空,則為此預設結果。
  • Accumulator 累加器:具有兩個引數的函式:歸約reduce運算後的部分結果和流的下一個元素
  • Combiner 組合器:當歸約是並行化或累加器引數的型別與累加器實現的型別不匹配時,用於合併combine歸約操作的部分結果的函式

可以轉為reduce的for-loop:

List<String> letters = Arrays.asList("a","bb","ccc");
boolean seen = false;
String acc = null;
for (String letter : letters) {
    if (!seen) {
        seen = true;
        acc = letter;
    } else {
        acc = acc.length() < letter.length() ? acc : letter;
    }
}

對應的reduce函式:

letters.stream()
       .reduce((partialString, element) -> {
           System.out.println(partialString + " " + element);
           return partialString.length() < element.length() ? partialString : element;
       }).get();

// output
a bb
a ccc

  • 這裡partialString是identity標識,它儲存reduce的初始值,也是stream是空的預設結果;
  • partialString後面是一個累加器,它會將使用partialString和Stream中一個元素實現運算獲得結果;



當Stream並行執行時,Java執行時會將流拆分為多個子流。在這種情況下,我們需要使用一種函式將子流的結果合併為一個。 這就是組合器的作用。

List<Integer> ages = Arrays.asList(25, 30, 45, 28, 32);
int computedAges = ages.parallelStream().reduce(0, a, b -> a + b, Integer::sum);

上面Integer :: sum方法是組合器。
如果我們使用順序流並且累加器引數的型別和其實現的型別匹配,則無需使用組合器。
 
使用並行流時,應確保reduce()或在流上執行的任何其他聚合累積操作是:
  • associative互換的:結果不受運算元順序的影響
    • 無干擾:操作不會影響資料來源
      • 無狀態和確定性的:該操作沒有狀態,並且對於給定的輸入產生相同的輸出


我們應該滿足所有這些條件,以防止出現不可預測的結果。
並行流比順序流的效能要好得多,當我們需要使用大型流並執行昂貴的聚合操作時,並行化流是正確的方法。
 
看看下面是一個字串合併的reduce不正確用法:

 public String concatenate(List<Character> chars) {
        return chars.stream()
                    .reduce(new StringBuilder(""), (acc, c) -> acc.append(c)).toString();
    }

上面reduce兩個引數:identity標識引數、累積器accumulator引數。
注意:這裡用法是不正確的,reduce對累加器有要求的,應該是可交換、無干擾和無狀態的,由於這裡identity標識new StringBuilder("")是可變的,這個初始值因為追加新的字元變化,因此在並行執行的情況下,結果將被破壞。
修復:

public static String concatenate(List<Character> chars) {
        return chars
                .stream()
                .reduce(new StringBuilder(),
                                StringBuilder::append,
                                StringBuilder::append).toString();
    }


它類似下面程式碼:

U result = identity;
for (T element : this stream)
     result = accumulator.apply(result, element)
return result;


上面做法效能不好!這樣的實現將進行大量的字串複製,並且執行時間的字元數將為O(n ^ 2)。一種更有效的方法是將結果累積到中StringBuilder,這是用於累積字串的可變容器。我們可以像使用普通歸約法一樣使用相同的技術來並行化可變歸約法。
可變歸約運算稱為 collect(),因為它將所需的結果收集到結果容器中Collection。
 

reduce與collect
與reduce方法(該方法在處理元素時總是會建立一個新值)不同,collect方法會修改或變異現有值。一個collect操作需要三個函式引數:

  • supplier供應商函式,用於構造結果容器的新例項;
    • 累加器功能,用於將輸入元素合併到結果容器中;
      • 以及組合combiner功能,用於將一個結果容器中的內容合併到另一個結果容器中。



 <R> R collect(Supplier<R> supplier,
               BiConsumer<R, ? super T> accumulator,
               BiConsumer<R, R> combiner);


與reduce()一樣,collect以這種抽象方式表示的好處是它直接適合於並行化:只要累加和組合函式滿足適當的要求,我們就可以並行累加部分結果,然後將它們組合起來。例如,要將流中元素的String表示形式收集到中ArrayList,我們可以編寫明顯的順序for-each形式:

ArrayList<String> strings = new ArrayList<>();
for (T element : stream) {                 
        strings.add(element.toString());
}


或者我們可以使用可並行化的收集形式:

ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                              (c, e) -> c.add(e.toString()),
                               (c1, c2) -> c1.addAll(c2));


或者,將map操作從累加器函式中拉出,我們可以更簡潔地表示為:

List<String> strings = stream.map(Object::toString)
               .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);


我們的供應商就是ArrayList constructor,累加器將字串化的元素新增到ArrayList中 ,而addAll 合併器只是用來將字串從一個容器複製到另一個容器中。
這個程式碼實際是一個Map/reduce模式。
 

reduce vs. fold
這兩個函式之間的區別在於,fold()取一個初始值並將其用作第一步的累加值,而reduce()的第一步則將第一和第二個元素用作第一步的操作引數。kotlin程式碼:

val numbers = listOf(5, 2, 10, 4)

val sum = numbers.reduce { sum, element -> sum + element }
println(sum)
val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 }
println(sumDoubled)

//val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 } //incorrect: the first element isn't doubled in the result
//println(sumDoubledReduce)


fold()用於計算加倍元素的總和。如果將相同的函式傳遞給reduce(),則它將返回另一個結果,因為它在第一步使用列表的第一個和第二個元素作為引數,因此第一個元素不會被加倍。
java中collect類似kotlin的fold。
 
網友啟發觀點:
reduce實際是累計sum或Accumulator!
是不是類似SQL中5個巢狀的JOINS和GROUP BY?
它實際是一直尾遞tail-recursive功能!

相關文章