A resposta de Eran descreveu as diferenças entre as versões de dois e três argumentos reduce
em que a primeira se reduz Stream<T>
a T
enquanto a segunda se reduz Stream<T>
a U
. No entanto, ele realmente não explicou a necessidade da função combinadora adicional ao reduzir Stream<T>
para U
.
Um dos princípios de design da API do Streams é que a API não deve diferir entre fluxos sequenciais e paralelos ou, dito de outra forma, uma API específica não deve impedir que um fluxo seja executado corretamente sequencialmente ou em paralelo. Se suas lambdas tiverem as propriedades corretas (associativas, não interferentes, etc.), um fluxo executado sequencialmente ou em paralelo deverá fornecer os mesmos resultados.
Vamos primeiro considerar a versão de redução de dois argumentos:
T reduce(I, (T, T) -> T)
A implementação seqüencial é direta. O valor da identidade I
é "acumulado" com o elemento de fluxo zeroth para fornecer um resultado. Esse resultado é acumulado com o primeiro elemento de fluxo para fornecer outro resultado, que por sua vez é acumulado com o segundo elemento de fluxo e assim por diante. Depois que o último elemento é acumulado, o resultado final é retornado.
A implementação paralela começa dividindo o fluxo em segmentos. Cada segmento é processado por seu próprio encadeamento da maneira sequencial descrita acima. Agora, se tivermos N threads, teremos N resultados intermediários. Estes precisam ser reduzidos a um resultado. Como cada resultado intermediário é do tipo T e temos vários, podemos usar a mesma função acumuladora para reduzir esses N resultados intermediários para um único resultado.
Agora vamos considerar uma operação hipotética de redução de dois argumentos que reduz Stream<T>
a U
. Em outros idiomas, isso é chamado de operação "fold" ou "fold-left", e é assim que chamarei aqui. Observe que isso não existe em Java.
U foldLeft(I, (U, T) -> U)
(Observe que o valor da identidade I
é do tipo U.)
A versão sequencial de foldLeft
é exatamente reduce
igual à versão seqüencial, exceto que os valores intermediários são do tipo U em vez do tipo T. Mas, caso contrário, é o mesmo. (Uma foldRight
operação hipotética seria semelhante, exceto que as operações seriam executadas da direita para a esquerda em vez de da esquerda para a direita.)
Agora considere a versão paralela de foldLeft
. Vamos começar dividindo o fluxo em segmentos. Podemos então fazer com que cada um dos N threads reduza os valores T em seu segmento em N valores intermediários do tipo U. E agora? Como chegamos de N valores do tipo U a um único resultado do tipo U?
O que está faltando é outra função que combina os vários resultados intermediários do tipo U em um único resultado do tipo U. Se tivermos uma função que combine dois valores de U em um, isso é suficiente para reduzir qualquer número de valores para um - assim como a redução original acima. Portanto, a operação de redução que resulta em um tipo diferente precisa de duas funções:
U reduce(I, (U, T) -> U, (U, U) -> U)
Ou, usando a sintaxe Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Em resumo, para fazer uma redução paralela a um tipo de resultado diferente, precisamos de duas funções: uma que acumule elementos T para valores U intermediários e uma segunda que combine os valores U intermediários em um único resultado U. Se não estamos trocando de tipo, acontece que a função acumuladora é a mesma que a função combinadora. É por isso que a redução para o mesmo tipo possui apenas a função de acumulador e a redução para um tipo diferente requer funções separadas de acumulador e combinador.
Finalmente, Java não fornece foldLeft
e foldRight
operações porque implicam uma ordem particular de operações que é inerentemente sequencial. Isso entra em conflito com o princípio de design declarado acima, ao fornecer APIs que suportam igualmente a operação sequencial e paralela.