Perché è necessario un combinatore per ridurre il metodo che converte il tipo in Java 8


142

Sto riscontrando problemi nel comprendere appieno il ruolo che ricopre combinernel reducemetodo Stream .

Ad esempio, il seguente codice non viene compilato:

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

Errore di compilazione che dice: (argomento non corrispondente; int non può essere convertito in java.lang.String)

ma questo codice compila:

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

Capisco che il metodo combinatore viene utilizzato in flussi paralleli, quindi nel mio esempio sta sommando due ints intermedi accumulati.

Ma non capisco perché il primo esempio non si compili senza il combinatore o come il combinatore sta risolvendo la conversione della stringa in int poiché sta solo sommando due ints.

Qualcuno può far luce su questo?



2
aha, è per flussi paralleli ... chiamo astrazione che perde!
Andy,

Risposte:


77

Le versioni a due e tre argomenti di reducecui si è tentato di utilizzare non accettano lo stesso tipo per accumulator.

I due argomenti reducesono definiti come :

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

Nel tuo caso, T è String, quindi BinaryOperator<T>dovrebbe accettare due argomenti String e restituire una stringa. Ma gli passi un int e una String, il che provoca l'errore di compilazione che hai - argument mismatch; int cannot be converted to java.lang.String. In realtà, penso che passare 0 come valore dell'identità sia sbagliato anche qui, poiché è prevista una stringa (T).

Si noti inoltre che questa versione di ridurre elabora un flusso di Ts e restituisce una T, quindi non è possibile utilizzarla per ridurre un flusso di String a un int.

I tre argomenti reducesono definiti come :

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

Nel tuo caso U è intero e T è stringa, quindi questo metodo ridurrà un flusso di stringa a un numero intero.

Per l' BiFunction<U,? super T,U>accumulatore puoi passare parametri di due tipi diversi (U e? Super T), che nel tuo caso sono Integer e String. Inoltre, il valore dell'identità U accetta un numero intero nel tuo caso, quindi passarlo a 0 va bene.

Un altro modo per ottenere ciò che vuoi:

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

Qui il tipo di flusso corrisponde al tipo restituito di reduce, quindi è possibile utilizzare la versione con due parametri di reduce.

Ovviamente non devi usare reduceaffatto:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();

8
Come seconda opzione nel tuo ultimo codice, potresti anche usare mapToInt(String::length)over mapToInt(s -> s.length()), non sono sicuro se uno sarebbe meglio rispetto all'altro, ma preferisco il primo per la leggibilità.
Skiwi,

20
Molti troveranno questa risposta perché non capiscono perché combinerè necessario, perché non averne accumulatorabbastanza. In tal caso: il combinatore è necessario solo per flussi paralleli, per combinare i risultati "accumulati" dei fili.
ddekany,

1
Non trovo la tua risposta particolarmente utile - perché non spieghi affatto cosa dovrebbe fare il combinatore e come posso lavorare senza di essa! Nel mio caso, voglio ridurre un tipo T a una U, ma non è possibile farlo in parallelo. Semplicemente non è possibile. Come si dice al sistema che non voglio / ho bisogno del parallelismo e quindi tralasciare il combinatore?
Zordid,

@Zordid l'API Streams non include un'opzione per ridurre il tipo T a una U senza passare un combinatore.
Eran,

216

La risposta di Eran ha descritto le differenze tra le versioni reducea due e tre arg in cui la prima si riduce Stream<T>a Tmentre la seconda Stream<T>a U. Tuttavia, in realtà non ha spiegato la necessità della funzione combinatrice aggiuntiva quando si riduce Stream<T>a U.

Uno dei principi di progettazione dell'API Streams è che l'API non dovrebbe differire tra flussi sequenziali e paralleli o, in altre parole, un'API particolare non dovrebbe impedire a un flusso di funzionare correttamente in sequenza o in parallelo. Se i tuoi lambda hanno le proprietà giuste (associative, non interferenti, ecc.) Un flusso eseguito in sequenza o in parallelo dovrebbe dare gli stessi risultati.

Consideriamo innanzitutto la versione a due argomenti della riduzione:

T reduce(I, (T, T) -> T)

L'implementazione sequenziale è semplice. Il valore dell'identità Iviene "accumulato" con l'elemento stream zeroth per dare un risultato. Questo risultato viene accumulato con il primo elemento stream per dare un altro risultato, che a sua volta viene accumulato con il secondo elemento stream e così via. Dopo aver accumulato l'ultimo elemento, viene restituito il risultato finale.

L'implementazione parallela inizia partendo il flusso in segmenti. Ogni segmento viene elaborato dal proprio thread nel modo sequenziale che ho descritto sopra. Ora, se abbiamo N thread, abbiamo N risultati intermedi. Questi devono essere ridotti fino a un risultato. Poiché ogni risultato intermedio è di tipo T, e ne abbiamo diversi, possiamo usare la stessa funzione di accumulatore per ridurre i risultati N intermedi fino a un singolo risultato.

Consideriamo ora un'ipotetica operazione di riduzione a due arg che si riduce Stream<T>a U. In altre lingue, questa operazione viene chiamata operazione "piega" o "piega a sinistra", quindi è quello che chiamerò qui. Nota che questo non esiste in Java.

U foldLeft(I, (U, T) -> U)

(Si noti che il valore dell'identità Iè di tipo U.)

La versione sequenziale di foldLeftè proprio come la versione sequenziale di reducetranne per il fatto che i valori intermedi sono di tipo U anziché di tipo T. Ma per il resto è lo stesso. (Un'ipotetica foldRightoperazione sarebbe simile tranne per il fatto che le operazioni sarebbero state eseguite da destra a sinistra anziché da sinistra a destra.)

Ora considera la versione parallela di foldLeft. Cominciamo dividendo il flusso in segmenti. Possiamo quindi far sì che ciascuno dei N thread riduca i valori T nel suo segmento in N valori intermedi di tipo U. E adesso? Come possiamo ottenere da N valori di tipo U fino a un singolo risultato di tipo U?

Ciò che manca è un'altra funzione che combina i risultati intermedi multipli di tipo U in un singolo risultato di tipo U. Se abbiamo una funzione che combina due valori U in uno, è sufficiente ridurre un numero qualsiasi di valori fino a uno, proprio come la riduzione originale sopra. Pertanto, l'operazione di riduzione che fornisce un risultato di tipo diverso richiede due funzioni:

U reduce(I, (U, T) -> U, (U, U) -> U)

Oppure, usando la sintassi Java:

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

In sintesi, per ridurre in parallelo un diverso tipo di risultato, abbiamo bisogno di due funzioni: una che accumula elementi T in valori U intermedi e una seconda che combina i valori U intermedi in un singolo risultato U. Se non stiamo cambiando tipo, si scopre che la funzione accumulatore è la stessa della funzione combinatore. Ecco perché la riduzione allo stesso tipo ha solo la funzione di accumulatore e la riduzione a un tipo diverso richiede funzioni di accumulatore e combinatore separate.

Infine, Java non fornisce foldLefte foldRightoperazioni perché implicano un particolare ordinamento di operazioni intrinsecamente sequenziali. Ciò si scontra con il principio di progettazione sopra indicato di fornire API che supportano allo stesso modo il funzionamento sequenziale e parallelo.


7
Quindi cosa puoi fare se hai bisogno di un foldLeftperché il calcolo dipende dal risultato precedente e non può essere parallelizzato?
amebe,

5
@amoebe Puoi implementare il tuo foldLeft usando forEachOrdered. Tuttavia, lo stato intermedio deve essere mantenuto in una variabile acquisita.
Stuart Marks

@StuartMarks grazie, ho finito per usare jOOλ. Hanno un'implementazionefoldLeft ordinata di .
amebe,

1
Adoro questa risposta! Correggimi se sbaglio: questo spiega perché l'esempio corrente di OP (il secondo) non invocherà mai il combinatore, quando eseguito, essendo il sequenziale del flusso.
Luigi Cortese,

2
Spiega quasi tutto ... tranne: perché questo dovrebbe escludere una riduzione basata su sequenze. Nel mio caso è IMPOSSIBILE farlo in parallelo poiché la mia riduzione riduce un elenco di funzioni in una U chiamando ciascuna funzione sul risultato intermedio del risultato precedente. Questo non può essere fatto in parallelo e non c'è modo di descrivere un combinatore. Quale metodo posso usare per raggiungere questo obiettivo?
Zordid,

116

Dato che mi piacciono gli scarabocchi e le frecce per chiarire i concetti ... iniziamo!

Da stringa a stringa (flusso sequenziale)

Supponiamo di avere 4 stringhe: il tuo obiettivo è concatenare tali stringhe in una. Fondamentalmente inizi con un tipo e finisci con lo stesso tipo.

Puoi farlo con

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

e questo ti aiuta a visualizzare ciò che sta accadendo:

inserisci qui la descrizione dell'immagine

La funzione accumulatore converte, passo dopo passo, gli elementi nel flusso (rosso) nel valore finale (verde) ridotto. La funzione di accumulatore trasforma semplicemente un Stringoggetto in un altro String.

Da String a int (flusso parallelo)

Supponiamo di avere le stesse 4 stringhe: il tuo nuovo obiettivo è quello di sommare le loro lunghezze e vuoi parallelizzare il tuo stream.

Ciò di cui hai bisogno è qualcosa del genere:

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

e questo è uno schema di ciò che sta accadendo

inserisci qui la descrizione dell'immagine

Qui la funzione accumulatore (a BiFunction) ti consente di trasformare i tuoi Stringdati in intdati. Essendo il flusso parallelo, è diviso in due parti (rosse), ognuna delle quali viene elaborata indipendentemente l'una dall'altra e produce altrettanti risultati parziali (arancioni). È necessario definire un combinatore per fornire una regola per unire i intrisultati parziali a quello finale (verde) int.

Da String a int (flusso sequenziale)

E se non vuoi parallelizzare il tuo stream? Bene, un combinatore deve essere fornito comunque, ma non verrà mai invocato, dato che non verranno prodotti risultati parziali.


7
Grazie per questo. Non avevo nemmeno bisogno di leggere. Vorrei che avessero appena aggiunto una funzione di piegatura eccentrica.
Lodewijk Bogaards,

1
@LodewijkBogaards felice che ti sia stato d'aiuto! JavaDoc qui è piuttosto criptico
Luigi Cortese il

@LuigiCortese Nel flusso parallelo divide sempre gli elementi in coppie?
TheLogicGuy,

1
Apprezzo la tua risposta chiara e utile. Voglio ripetere un po 'di quello che hai detto: "Beh, un combinatore deve essere fornito comunque, ma non verrà mai invocato". Questo fa parte della programmazione funzionale di Brave New World of Java che, mi è stato assicurato innumerevoli volte, "rende il tuo codice più conciso e più facile da leggere". Speriamo che esempi di (virgolette) chiarezza concisa come questa rimangano pochi e lontani tra loro.
dnuttle,

Sarà MOLTO meglio illustrare riduci con otto stringhe ...
Ekaterina Ivanova iceja.net

0

Non esiste una versione ridotta che accetta due tipi diversi senza un combinatore poiché non può essere eseguita in parallelo (non sono sicuro del motivo per cui questo è un requisito). Il fatto che l' accumulatore debba essere associativo rende questa interfaccia praticamente inutile poiché:

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

Produce gli stessi risultati di:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);

Tale maptrucco dipende dal particolare accumulatore combinerpuò rallentare le cose praticamente.
Tagir Valeev,

Oppure, accelera in modo significativo poiché ora puoi semplificare accumulatorfacendo cadere il primo parametro.
quiz123

La riduzione parallela è possibile, dipende dal tuo calcolo. Nel tuo caso, devi essere consapevole della complessità del combinatore ma anche dell'accumulatore sull'identità rispetto ad altri casi.
LoganMzz,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.