Odpowiedź Erana opisuje różnice między wersjami dwuargumentowymi i trójargumentowymi reduce
w tym, że pierwsza redukuje się Stream<T>
do, T
podczas gdy druga Stream<T>
do U
. Jednak w rzeczywistości nie wyjaśnia to potrzeby dodatkowej funkcji sumatora podczas redukcji Stream<T>
do U
.
Jedną z zasad projektowych interfejsu Streams API jest to, że interfejs API nie powinien różnić się między strumieniami sekwencyjnymi i równoległymi lub inaczej mówiąc, określony interfejs API nie powinien uniemożliwiać poprawnego działania strumienia sekwencyjnie lub równolegle. Jeśli Twoje lambdy mają odpowiednie właściwości (asocjacyjne, niezakłócające itp.), Strumień uruchamiany sekwencyjnie lub równolegle powinien dawać te same wyniki.
Najpierw rozważmy dwuargumentową wersję redukcji:
T reduce(I, (T, T) -> T)
Implementacja sekwencyjna jest prosta. Wartość tożsamości I
jest „kumulowana” z zerowym elementem stream w celu uzyskania wyniku. Wynik ten jest gromadzony z pierwszym elementem strumienia, aby dać inny wynik, który z kolei jest gromadzony z drugim elementem strumienia i tak dalej. Po skumulowaniu ostatniego elementu zwracany jest wynik końcowy.
Wdrożenie równoległe rozpoczyna się od podzielenia strumienia na segmenty. Każdy segment jest przetwarzany przez swój własny wątek w sposób sekwencyjny, który opisałem powyżej. Teraz, jeśli mamy N wątków, mamy N wyników pośrednich. Należy je zredukować do jednego wyniku. Ponieważ każdy wynik pośredni jest typu T, a mamy ich kilka, możemy użyć tej samej funkcji akumulatora, aby zredukować te wyniki pośrednie N do jednego wyniku.
Rozważmy teraz hipotetyczną dwuargumentową operację redukcji, która sprowadza się Stream<T>
do U
. W innych językach nazywa się to operacją „zagięcia” lub „zagięcia w lewo”, więc tak to tutaj nazywam. Zauważ, że to nie istnieje w Javie.
U foldLeft(I, (U, T) -> U)
(Zwróć uwagę, że wartość tożsamości I
jest typu U.)
Sekwencyjna wersja programu foldLeft
jest taka sama, jak sekwencyjna wersja programu, reduce
z tą różnicą, że wartości pośrednie są typu U zamiast typu T. Poza tym jest to to samo. (Hipotetyczna foldRight
operacja byłaby podobna, z tą różnicą, że operacje byłyby wykonywane od prawej do lewej zamiast od lewej do prawej).
Rozważmy teraz równoległą wersję foldLeft
. Zacznijmy od podzielenia strumienia na segmenty. Możemy zatem sprawić, że każdy z N wątków zredukuje wartości T w swoim segmencie do N pośrednich wartości typu U. I co teraz? Jak uzyskać od N wartości typu U do pojedynczego wyniku typu U?
Brakuje innej funkcji, która łączy wiele wyników pośrednich typu U w jeden wynik typu U. Jeśli mamy funkcję, która łączy dwie wartości U w jedną, to wystarczy, aby zredukować dowolną liczbę wartości do jednej - tak jak oryginalna redukcja powyżej. Zatem operacja redukcji, która daje wynik innego typu, wymaga dwóch funkcji:
U reduce(I, (U, T) -> U, (U, U) -> U)
Lub używając składni Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Podsumowując, aby przeprowadzić równoległą redukcję do innego typu wyniku, potrzebujemy dwóch funkcji: jednej, która gromadzi elementy T do pośrednich wartości U, i drugiej, która łączy pośrednie wartości U w jeden wynik U. Jeśli nie zmieniamy typów, okazuje się, że funkcja akumulatora jest taka sama jak funkcja sumatora. Dlatego redukcja do tego samego typu ma tylko funkcję akumulatora, a redukcja do innego typu wymaga oddzielnych funkcji akumulatora i sumatora.
Wreszcie, Java nie udostępnia operacji foldLeft
i foldRight
, ponieważ implikują one określoną kolejność operacji, która jest z natury sekwencyjna. Jest to sprzeczne z zasadą projektowania opisaną powyżej, polegającą na zapewnianiu interfejsów API, które w równym stopniu obsługują operacje sekwencyjne i równoległe.