Dlaczego potrzebny jest łącznik do metody redukującej, która konwertuje typ w java 8

142

Mam problem z pełnym zrozumieniem roli, jaką combinerspełnia reducemetoda strumieniowa .

Na przykład następujący kod nie kompiluje się:

int length = asList("str1", "str2").stream()
            .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());

Komunikat o błędzie kompilacji: (niezgodność argumentów; nie można przekonwertować wartości int na java.lang.String)

ale ten kod się kompiluje:

int length = asList("str1", "str2").stream()  
    .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), 
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);

Rozumiem, że metoda sumatora jest używana w równoległych strumieniach - więc w moim przykładzie jest to sumowanie dwóch pośrednich skumulowanych int.

Ale nie rozumiem, dlaczego pierwszy przykład nie kompiluje się bez sumatora lub w jaki sposób sumator rozwiązuje konwersję ciągu znaków na int, ponieważ jest to po prostu dodanie dwóch liczb całkowitych.

Czy ktoś może rzucić na to światło?

Louise Miller
źródło
Powiązane pytanie: stackoverflow.com/questions/24202473/…
nosid
2
aha, to dla równoległych strumieni ... nazywam nieszczelną abstrakcją!
Andy

Odpowiedzi:

77

Wersje dwu- i trzyargumentowe, reducektórych próbowano użyć, nie akceptują tego samego typu dla accumulator.

Te dwa argumenty reducezdefiniowane jako :

T reduce(T identity,
         BinaryOperator<T> accumulator)

W twoim przypadku T jest Stringiem, więc BinaryOperator<T>powinno przyjąć dwa argumenty typu String i zwrócić String. Ale przekazujesz do niego int i String, co powoduje błąd kompilacji - argument mismatch; int cannot be converted to java.lang.String. Właściwie myślę, że przekazywanie 0, ponieważ wartość tożsamości jest również tutaj błędne, ponieważ oczekuje się String (T).

Zauważ również, że ta wersja redukuje przetwarza strumień Ts i zwraca T, więc nie możesz jej użyć do zredukowania strumienia String do int.

Te trzy argumenty reducezdefiniowane jako :

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

W twoim przypadku U to Integer, a T to String, więc ta metoda zredukuje strumień String do Integer.

Dla BiFunction<U,? super T,U> akumulatora można przekazać parametry dwóch różnych typów (U i? Super T), którymi w twoim przypadku są Integer i String. Ponadto wartość tożsamości U akceptuje w twoim przypadku liczbę całkowitą, więc przekazanie jej 0 jest w porządku.

Inny sposób na osiągnięcie tego, co chcesz:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);

Tutaj typ strumienia pasuje do zwracanego typu reduce, więc możesz użyć wersji z dwoma parametrami reduce.

Oczywiście nie musisz reducew ogóle używać :

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();
Eran
źródło
8
Jako drugą opcję w ostatnim kodzie możesz również użyć mapToInt(String::length)over mapToInt(s -> s.length()), nie jestem pewien, czy jeden byłby lepszy od drugiego, ale wolę ten pierwszy ze względu na czytelność.
skiwi
20
Wielu znajdzie tę odpowiedź, ponieważ nie rozumieją, dlaczego combinerjest to potrzebne, dlaczego nie accumulatorwystarczy. W takim przypadku: Łącznik jest potrzebny tylko dla równoległych strumieni, aby połączyć „nagromadzone” wyniki wątków.
ddekany
1
Twoja odpowiedź nie jest dla mnie szczególnie przydatna - ponieważ w ogóle nie wyjaśniasz, co powinien zrobić sumator i jak mogę bez niego pracować! W moim przypadku chcę zredukować typ T do U, ale nie ma sposobu, aby w ogóle można było to zrobić równolegle. To po prostu niemożliwe. Jak powiedzieć systemowi, że nie chcę / potrzebuję równoległości, a tym samym pomijam sumator?
Zordid
@Zordid Streams API nie zawiera opcji redukcji typu T do U bez przekazywania sumatora.
Eran
216

Odpowiedź Erana opisuje różnice między wersjami dwuargumentowymi i trójargumentowymi reducew tym, że pierwsza redukuje się Stream<T>do, Tpodczas 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 Ijest „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 Ijest typu U.)

Sekwencyjna wersja programu foldLeftjest taka sama, jak sekwencyjna wersja programu, reducez tą różnicą, że wartości pośrednie są typu U zamiast typu T. Poza tym jest to to samo. (Hipotetyczna foldRightoperacja 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 foldLefti 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.

Stuart Marks
źródło
7
Więc co możesz zrobić, jeśli potrzebujesz a, foldLeftponieważ obliczenia zależą od poprzedniego wyniku i nie można ich zrównoleglać?
ameba
5
@amoebe Możesz zaimplementować swój własny foldLeft za pomocą forEachOrdered. Stan pośredni należy jednak zachować w przechwyconej zmiennej.
Stuart Marks
@StuartMarks dzięki, skończyło się na używaniu jOOλ. Mają zgrabną implementacjęfoldLeft .
ameba
1
Uwielbiam tę odpowiedź! Popraw mnie, jeśli się mylę: to wyjaśnia, dlaczego działający przykład OP (drugi) nigdy nie wywoła sumatora po uruchomieniu, będąc sekwencyjnym strumieniem.
Luigi Cortese
2
Wyjaśnia prawie wszystko ... z wyjątkiem: dlaczego miałoby to wykluczać redukcję opartą na sekwencjach. W moim przypadku jest NIEMOŻLIWE, aby zrobić to równolegle, ponieważ moja redukcja redukuje listę funkcji do U, wywołując każdą funkcję na pośrednim wyniku wyniku jej poprzedników. W ogóle nie można tego zrobić równolegle i nie ma sposobu, aby opisać sumator. Jakiej metody mogę użyć, aby to osiągnąć?
Zordid
116

Ponieważ lubię gryzmoły i strzałki, aby wyjaśnić koncepcje, zacznijmy!

Od ciągu do ciągu (strumień sekwencyjny)

Załóżmy, że masz 4 ciągi: Twoim celem jest połączenie takich ciągów w jeden. Zasadniczo zaczynasz od typu i kończysz tym samym typem.

Możesz to osiągnąć dzięki

String res = Arrays.asList("one", "two","three","four")
        .stream()
        .reduce("",
                (accumulatedStr, str) -> accumulatedStr + str);  //accumulator

a to pomaga w wizualizacji tego, co się dzieje:

wprowadź opis obrazu tutaj

Funkcja akumulatora konwertuje, krok po kroku, elementy w (czerwonym) strumieniu do końcowej zredukowanej (zielonej) wartości. Funkcja akumulatora po prostu przekształca Stringobiekt w inny String.

Od ciągu do int (strumień równoległy)

Załóżmy, że mamy te same 4 ciągi: Twoim nowym celem jest zsumowanie ich długości i chcesz zrównoleglenie strumienia.

Potrzebujesz czegoś takiego:

int length = Arrays.asList("one", "two","three","four")
        .parallelStream()
        .reduce(0,
                (accumulatedInt, str) -> accumulatedInt + str.length(),                 //accumulator
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner

a to jest schemat tego, co się dzieje

wprowadź opis obrazu tutaj

Tutaj funkcja akumulatora (a BiFunction) umożliwia przekształcenie Stringdanych w intdane. Ponieważ strumień jest równoległy, jest podzielony na dwie (czerwone) części, z których każda jest opracowywana niezależnie od siebie i daje tyle samo wyników częściowych (pomarańczowych). Zdefiniowanie sumatora jest potrzebne, aby zapewnić regułę scalania intwyników częściowych w końcowy (zielony) int.

Od ciągu do int (strumień sekwencyjny)

A co jeśli nie chcesz zrównoleglać swojego strumienia? Cóż, i tak trzeba podać sumator, ale nigdy nie zostanie on wywołany, biorąc pod uwagę, że nie zostaną wytworzone żadne częściowe wyniki.

Luigi Cortese
źródło
7
Dzięki za to. Nie musiałem nawet czytać. Żałuję, że nie dodali po prostu cholernej funkcji składania.
Lodewijk Bogaards
1
@LodewijkBogaards cieszę się, że pomogło! JavaDoc tutaj jest rzeczywiście dość tajemnicza
Luigi Cortese
@LuigiCortese Czy w strumieniu równoległym zawsze dzieli elementy na pary?
TheLogicGuy
1
Doceniam twoją jasną i użyteczną odpowiedź. Chcę trochę powtórzyć to, co powiedziałeś: „Cóż, i tak trzeba dostarczyć sumator, ale nigdy nie zostanie on wywołany”. Jest to część programowania funkcjonalnego Brave New World of Java, o której zapewniano mnie niezliczoną ilość razy, że „sprawia, że ​​kod jest bardziej zwięzły i łatwiejszy do odczytania”. Miejmy nadzieję, że przykłady (cudzysłowu) zwięzłej jasności, takie jak ta, pozostają nieliczne.
dnuttle
ZNACZNIE lepiej będzie zilustrować redukcję ośmioma strunami ...
Ekaterina Ivanova iceja.net
0

Nie ma wersji redukującej, która przyjmuje dwa różne typy bez sumatora, ponieważ nie można jej wykonać równolegle (nie wiem, dlaczego jest to wymagane). Fakt, że akumulator musi być asocjacyjny, sprawia, że ​​ten interfejs jest praktycznie bezużyteczny, ponieważ:

list.stream().reduce(identity,
                     accumulator,
                     combiner);

Daje takie same wyniki jak:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);
quiz123
źródło
Taka mapsztuczka zależy od konkretnej sytuacji accumulatori combinermoże znacznie spowolnić działanie.
Tagir Valeev
Lub znacznie przyspieszyć, ponieważ możesz teraz uprościć accumulator, pomijając pierwszy parametr.
quiz123
Równoległa redukcja jest możliwa, zależy to od twoich obliczeń. W twoim przypadku musisz być świadomy złożoności sumatora, ale także akumulatora na tożsamości w porównaniu z innymi instancjami.
LoganMzz