¿Por qué debería usar "operaciones funcionales" en lugar de un bucle for?

39
for (Canvas canvas : list) {
}

NetBeans me sugiere usar "operaciones funcionales":

list.stream().forEach((canvas) -> {
});

Pero, ¿por qué se prefiere esto ? En todo caso, es más difícil de leer y entender. Está llamando stream(), luego forEach()utilizando una expresión lambda con parámetro canvas. No veo cómo es eso mejor que el forbucle en el primer fragmento.

Obviamente solo hablo por estética. Quizás hay una ventaja técnica aquí que me falta. ¿Qué es? ¿Por qué debería usar el segundo método en su lugar?

Omega
fuente
15
En su ejemplo particular, no sería preferible.
Robert Harvey
1
Mientras la única operación sea única para cada uno, tiendo a estar de acuerdo con usted. Tan pronto como agregue otras operaciones a la tubería, o produzca una secuencia de salida, entonces el enfoque de flujo se vuelve preferible.
JacquesB
@RobertHarvey, ¿no? ¿Por qué no?
sara
@RobertHarvey Bueno, estoy de acuerdo en que la respuesta aceptada realmente muestra cómo la versión for sale del agua para casos más complicados, pero no veo por qué las "victorias" en el caso trivial. Lo dices como si fuera evidente, pero no lo veo, así que pregunté.
sara

Respuestas:

45

Las secuencias proporcionan una abstracción mucho mejor para la composición de las diferentes operaciones que desea realizar además de las colecciones o secuencias de datos que ingresan. Especialmente cuando necesita asignar elementos, filtrarlos y convertirlos.

Tu ejemplo no es muy práctico. Considere el siguiente código del sitio de Oracle .

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}

se puede escribir usando secuencias:

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

La segunda opción es mucho más legible. Entonces, cuando tiene bucles anidados o varios bucles que realizan un procesamiento parcial, es un muy buen candidato para el uso de Streams / Lambda API.

luboskrnac
fuente
55
Existen técnicas de optimización como la fusión de construcción / reducción de fusión de mapas ( stream.map(f).map(g)stream.map(f.andThen(g))) (cuando se construye una secuencia en un método y luego se pasa a otro método que la consume, el compilador puede eliminar la secuencia) y la fusión de secuencia (que puede fusionar muchas operaciones de secuencia juntos en un solo ciclo imperativo), lo que puede hacer que las operaciones de transmisión sean mucho más eficientes. Se implementan en el compilador Haskell de GHC, y también en algunos otros compiladores de lenguaje funcional de Haskell y otros, y hay implementaciones de investigación experimental para Scala.
Jörg W Mittag
El rendimiento probablemente no sea un factor cuando se considera el bucle funcional frente al bucle, ya que seguramente el compilador podría / debería hacer la conversión de un bucle for a una operación funcional si Netbeans puede y se determina que es la ruta óptima.
Ryan
No estoy de acuerdo con que el segundo sea más legible. Lleva un tiempo darse cuenta de lo que está sucediendo. ¿Existe una ventaja de rendimiento al hacerlo el segundo método porque de lo contrario no lo veo?
Bok McDonagh
@BokMcDonagh, mi experiencia es que es menos legible para los desarrolladores que no se molestaron en familiarizarse con nuevas abstracciones. Sugeriría usar más estas API, para familiarizarse más, porque es el futuro. No solo en el mundo Java.
luboskrnac
16

Otra ventaja de usar la API de transmisión funcional es que oculta los detalles de la implementación. Solo describe lo que debe hacerse, no cómo. Esta ventaja se vuelve obvia cuando se observa el cambio que debe hacerse, para cambiar de ejecución de código único a paralelo. Solo cambia el .stream()a .parallelStream().

Stefan Dollase
fuente
13

En todo caso, es más difícil de leer y entender.

Eso es altamente subjetivo. Encuentro la segunda versión mucho más fácil de leer y entender. Coincide con cómo lo hacen otros idiomas (por ejemplo, Ruby, Smalltalk, Clojure, Io, Ioke, Seph), requiere menos conceptos para entender (es solo una llamada a un método normal como cualquier otro, mientras que el primer ejemplo es una sintaxis especializada).

En todo caso, es una cuestión de familiaridad.

Jörg W Mittag
fuente
66
Si eso es verdad. Pero esto parece más un comentario que una respuesta para mí.
Omega
La operación funcional también utiliza una sintaxis especializada: el "->" es nuevo
Ryan