La risposta di Eran ha descritto le differenze tra le versioni reduce
a due e tre arg in cui la prima si riduce Stream<T>
a T
mentre 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à I
viene "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 reduce
tranne per il fatto che i valori intermedi sono di tipo U anziché di tipo T. Ma per il resto è lo stesso. (Un'ipotetica foldRight
operazione 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 foldLeft
e foldRight
operazioni 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.