La respuesta de Eran describió las diferencias entre las versiones de dos argumentos y tres argumentos de reduce
que el primero se reduce Stream<T>
a T
mientras que el segundo se reduce Stream<T>
a U
. Sin embargo, en realidad no explicaba la necesidad de la función de combinador adicional cuando se reduce Stream<T>
a U
.
Uno de los principios de diseño de la API de Streams es que la API no debe diferir entre secuencias secuenciales y paralelas, o dicho de otra manera, una API en particular no debe evitar que una secuencia se ejecute correctamente de forma secuencial o en paralelo. Si sus lambdas tienen las propiedades correctas (asociativas, no interferentes, etc.), una secuencia ejecutada secuencialmente o en paralelo debería dar los mismos resultados.
Consideremos primero la versión de reducción de dos argumentos:
T reduce(I, (T, T) -> T)
La implementación secuencial es sencilla. El valor de identidad I
se "acumula" con el elemento de flujo cero para dar un resultado. Este resultado se acumula con el primer elemento de flujo para dar otro resultado, que a su vez se acumula con el segundo elemento de flujo, y así sucesivamente. Después de que se acumula el último elemento, se devuelve el resultado final.
La implementación paralela comienza dividiendo la secuencia en segmentos. Cada segmento es procesado por su propio hilo en la forma secuencial que describí anteriormente. Ahora, si tenemos N hilos, tenemos N resultados intermedios. Estos deben reducirse a un solo resultado. Como cada resultado intermedio es de tipo T, y tenemos varios, podemos usar la misma función de acumulador para reducir esos N resultados intermedios a un solo resultado.
Ahora consideremos una operación hipotética de reducción de dos argumentos que se reduce Stream<T>
a U
. En otros idiomas, esto se llama operación "doblar" o "doblar a la izquierda", así que así lo llamaré aquí. Tenga en cuenta que esto no existe en Java.
U foldLeft(I, (U, T) -> U)
(Tenga en cuenta que el valor de identidad I
es del tipo U).
La versión secuencial de foldLeft
es igual que la versión secuencial de, reduce
excepto que los valores intermedios son del tipo U en lugar del tipo T. Pero, por lo demás, es el mismo. (Una foldRight
operación hipotética sería similar, excepto que las operaciones se realizarían de derecha a izquierda en lugar de izquierda a derecha).
Ahora considere la versión paralela de foldLeft
. Comencemos dividiendo la secuencia en segmentos. Entonces podemos hacer que cada uno de los N hilos reduzca los valores de T en su segmento en N valores intermedios de tipo U. ¿Y ahora qué? ¿Cómo pasamos de N valores de tipo U a un solo resultado de tipo U?
Lo que falta es otra función que combine los múltiples resultados intermedios del tipo U en un solo resultado del tipo U. Si tenemos una función que combina dos valores U en uno, es suficiente para reducir cualquier número de valores a uno, al igual que La reducción original anterior. Por lo tanto, la operación de reducción que da un resultado de un tipo diferente necesita dos funciones:
U reduce(I, (U, T) -> U, (U, U) -> U)
O, usando la sintaxis de Java:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
En resumen, para hacer una reducción paralela a un tipo de resultado diferente, necesitamos dos funciones: una que acumule elementos T a valores U intermedios, y una segunda que combine los valores U intermedios en un único resultado U. Si no estamos cambiando tipos, resulta que la función del acumulador es la misma que la función del combinador. Es por eso que la reducción al mismo tipo solo tiene la función de acumulador y la reducción a un tipo diferente requiere funciones de acumulador y combinador separadas.
Finalmente, Java no proporciona operaciones foldLeft
y foldRight
operaciones porque implican un orden particular de operaciones que es inherentemente secuencial. Esto choca con el principio de diseño establecido anteriormente de proporcionar API que admitan la operación secuencial y paralela por igual.