¿Por qué se necesita un combinador para reducir el método que convierte el tipo en Java 8?

142

Tengo problemas para comprender completamente el papel que combinercumple el reducemétodo Streams .

Por ejemplo, el siguiente código no se compila:

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

El error de compilación dice: (discrepancia de argumento; int no se puede convertir a java.lang.String)

pero este código compila:

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

Entiendo que el método combinador se usa en corrientes paralelas, por lo que en mi ejemplo se suman dos entradas intermedias acumuladas.

Pero no entiendo por qué el primer ejemplo no se compila sin el combinador o cómo el combinador está resolviendo la conversión de string a int ya que solo está sumando dos ints.

¿Alguien puede arrojar luz sobre esto?

Louise Miller
fuente
Pregunta relacionada: stackoverflow.com/questions/24202473/…
nosid
2
aha, es para flujos paralelos ... ¡Yo llamo abstracción permeable!
Andy

Respuestas:

77

Las versiones de dos y tres argumentos reduceque intentó utilizar no aceptan el mismo tipo paraaccumulator .

Los dos argumentos reducese definen como :

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

En su caso, T es una cadena, por lo que BinaryOperator<T>debe aceptar dos argumentos de cadena y devolver una cadena. Pero le pasa un int y una cadena, lo que da como resultado el error de compilación que obtuvo - argument mismatch; int cannot be converted to java.lang.String. En realidad, creo que pasar 0 ya que el valor de identidad también está mal aquí, ya que se espera una Cadena (T).

También tenga en cuenta que esta versión de reduce procesa una secuencia de Ts y devuelve una T, por lo que no puede usarla para reducir una secuencia de String a un int.

Los tres argumentos reducese definen como :

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

En su caso, U es Entero y T es Cadena, por lo que este método reducirá una secuencia de Cadena a un Entero.

Para el BiFunction<U,? super T,U>acumulador puede pasar parámetros de dos tipos diferentes (U y? Super T), que en su caso son Integer y String. Además, el valor de identidad U acepta un número entero en su caso, por lo que pasarlo 0 está bien.

Otra forma de lograr lo que quieres:

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

Aquí el tipo de la secuencia coincide con el tipo de retorno de reduce, por lo que puede utilizar la versión de dos parámetros de reduce.

Por supuesto, no tienes que usar reducenada:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();
Eran
fuente
8
Como segunda opción en su último código, también puede usar mapToInt(String::length)over mapToInt(s -> s.length()), no estoy seguro de si uno sería mejor sobre el otro, pero prefiero el primero para facilitar la lectura.
skiwi
20
Muchos encontrarán esta respuesta ya que no entienden por qué combineres necesaria, por qué no tenerla accumulatores suficiente. En ese caso: el combinador solo es necesario para corrientes paralelas, para combinar los resultados "acumulados" de los hilos.
ddekany
1
No encuentro su respuesta particularmente útil, ¡porque no explican en absoluto lo que debe hacer el combinador y cómo puedo trabajar sin él! En mi caso, quiero reducir un tipo T a una U, pero no hay forma de que esto se pueda hacer en paralelo. Es simplemente imposible. ¿Cómo le dices al sistema que no quiero / necesito paralelismo y, por lo tanto, omito el combinador?
Zordid
@Zordid the Streams API no incluye una opción para reducir el tipo T a U sin pasar un combinador.
Eran
216

La respuesta de Eran describió las diferencias entre las versiones de dos argumentos y tres argumentos de reduceque el primero se reduce Stream<T>a Tmientras 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 Ise "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 Ies del tipo U).

La versión secuencial de foldLeftes igual que la versión secuencial de, reduceexcepto que los valores intermedios son del tipo U en lugar del tipo T. Pero, por lo demás, es el mismo. (Una foldRightoperació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 foldLefty foldRightoperaciones 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.

Stuart Marks
fuente
77
Entonces, ¿qué puede hacer si necesita un foldLeftporque el cálculo depende del resultado anterior y no se puede paralelizar?
amee
55
@amoebe Puede implementar su propio foldLeft usando forEachOrdered. Sin embargo, el estado intermedio debe mantenerse en una variable capturada.
Stuart Marks,
@StuartMarks gracias, terminé usando jOOλ. Tienen una buena implementación defoldLeft .
amee
1
Me encanta esta respuesta! Corrígeme si me equivoco: esto explica por qué el ejemplo de ejecución de OP (el segundo) nunca invocará al combinador, cuando se ejecuta, siendo la secuencia secuencial.
Luigi Cortese
2
Explica casi todo ... excepto: ¿por qué esto debería excluir la reducción basada secuencialmente? En mi caso, es IMPOSIBLE hacerlo en paralelo ya que mi reducción reduce una lista de funciones en una U llamando a cada función en el resultado intermedio del resultado de sus predecesores. Esto no se puede hacer en paralelo en absoluto y no hay forma de describir un combinador. ¿Qué método puedo usar para lograr esto?
Zordid
116

Como me gustan los garabatos y las flechas para aclarar conceptos ... ¡comencemos!

De cadena a cadena (secuencia secuencial)

Supongamos que tiene 4 cadenas: su objetivo es concatenar tales cadenas en una sola. Básicamente comienzas con un tipo y terminas con el mismo tipo.

Puedes lograr esto con

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

y esto te ayuda a visualizar lo que está sucediendo:

ingrese la descripción de la imagen aquí

La función de acumulador convierte, paso a paso, los elementos en su flujo (rojo) al valor final reducido (verde). La función de acumulador simplemente transforma un Stringobjeto en otro String.

De cadena a int (flujo paralelo)

Suponga que tiene las mismas 4 cadenas: su nuevo objetivo es sumar sus longitudes y desea paralelizar su flujo.

Lo que necesitas es algo como esto:

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

y este es un esquema de lo que está sucediendo

ingrese la descripción de la imagen aquí

Aquí la función de acumulador (a BiFunction) le permite transformar sus Stringdatos en intdatos. Al ser la corriente paralela, se divide en dos partes (rojas), cada una de las cuales se elabora independientemente una de la otra y produce la misma cantidad de resultados parciales (naranja). Se necesita definir un combinador para proporcionar una regla para fusionar intresultados parciales en el final (verde) int.

De cadena a int (secuencia secuencial)

¿Qué pasa si no quieres paralelizar tu transmisión? Bueno, de todos modos, se debe proporcionar un combinador, pero nunca se invocará, dado que no se producirán resultados parciales.

Luigi Cortese
fuente
77
Gracias por esto. Ni siquiera necesitaba leer. Desearía que hubieran agregado una función de doblez.
Lodewijk Bogaards
1
@LodewijkBogaards me alegro de que haya ayudado. JavaDoc aquí es bastante críptico de hecho
Luigi Cortese
@LuigiCortese En la secuencia paralela, ¿siempre divide los elementos en pares?
TheLogicGuy
1
Agradezco tu respuesta clara y útil. Quiero repetir un poco de lo que dijiste: "Bueno, de todos modos hay que proporcionar un combinador, pero nunca se invocará". Esto es parte de la programación funcional Brave New World of Java que, me han asegurado innumerables veces, "hace que su código sea más conciso y más fácil de leer". Esperemos que los ejemplos de (comillas de los dedos) sean claros y concisos, como este, son pocos y distantes entre sí.
dnuttle
Será MUCHO mejor ilustrar reducir con ocho cuerdas ...
Ekaterina Ivanova iceja.net
0

No existe reducir la versión que tiene dos tipos diferentes sin un combinador ya que no se puede ejecutar en paralelo (no estoy seguro de por qué esto es un requisito). El hecho de que el acumulador debe ser asociativo hace que esta interfaz sea bastante inútil ya que:

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

Produce los mismos resultados que:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);
quiz123
fuente
Tal maptruco depende de lo particular accumulatory combinerpuede ralentizar las cosas más o menos.
Tagir Valeev
O acelere significativamente, ya que ahora puede simplificar accumulatorsoltando el primer parámetro.
quiz123
La reducción paralela es posible, depende de su cálculo. En su caso, debe ser consciente de la complejidad del combinador pero también del acumulador de identidad frente a otras instancias.
LoganMzz