En Java 8, ¿es estilísticamente mejor usar expresiones de referencia de métodos o métodos que devuelven una implementación de la interfaz funcional?

11

Java 8 agregó el concepto de interfaces funcionales , así como numerosos métodos nuevos diseñados para tomar interfaces funcionales. Las instancias de estas interfaces se pueden crear de manera sucinta utilizando expresiones de referencia de método (por ejemplo SomeClass::someMethod) y expresiones lambda (por ejemplo (x, y) -> x + y).

Un colega y yo tenemos opiniones diferentes sobre cuándo es mejor usar una forma u otra (donde "mejor" en este caso realmente se reduce a "más legible" y "más acorde con las prácticas estándar", ya que básicamente son lo contrario equivalente). Específicamente, esto implica el caso en que todo lo siguiente es cierto:

  • la función en cuestión no se usa fuera de un solo alcance
  • dar un nombre a la instancia ayuda a la legibilidad (en lugar de, por ejemplo, que la lógica sea lo suficientemente simple como para ver lo que sucede de un vistazo)
  • No hay otras razones de programación por las cuales una forma sería preferible a la otra.

Mi opinión actual sobre el asunto es que agregar un método privado y referirlo por referencia de método es el mejor enfoque. Parece que así es como se diseñó la función para ser utilizada, y parece más fácil comunicar lo que está sucediendo a través de nombres y firmas de métodos (por ejemplo, "boolean isResultInFuture (Resultado del resultado)" claramente está diciendo que está devolviendo un booleano). También hace que el método privado sea más reutilizable si una mejora futura de la clase quiere hacer uso de la misma verificación, pero no necesita el envoltorio de interfaz funcional.

La preferencia de mi colega es tener un método que devuelva la instancia de la interfaz (por ejemplo, "Predicate resultInFuture ()"). Para mí, parece que no es exactamente cómo se pretendía utilizar la función, se siente un poco más complicado y parece que es más difícil realmente comunicar la intención a través del nombramiento.

Para concretar este ejemplo, aquí está el mismo código, escrito en los diferentes estilos:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

¿Existe alguna documentación oficial o semioficial o comentarios sobre si un enfoque es más preferido, más acorde con las intenciones de los diseñadores de idiomas o más legible? Salvo una fuente oficial, ¿hay alguna razón clara por la cual sería el mejor enfoque?

M. Justin
fuente
1
Se agregaron lambdas al lenguaje por una razón, y no fue para empeorar Java.
@Snowman Eso es evidente. Por lo tanto, supongo que hay alguna relación implícita entre su comentario y mi pregunta que no estoy entendiendo del todo. ¿Cuál es el punto que estás tratando de hacer?
M. Justin
2
Supongo que lo que está diciendo es que, si las lambdas no hubieran sido una mejora en el status quo, no se habrían molestado en presentarlas.
Robert Harvey, el
2
@RobertHarvey Claro. Sin embargo, hasta ese punto, se agregaron referencias lambdas y de método al mismo tiempo, por lo que tampoco lo fueron antes. Incluso sin este caso particular, hay momentos en que las referencias a métodos seguirían siendo mejores; por ejemplo, un método público existente (por ejemplo Object::toString). Entonces, mi pregunta es más acerca de si es mejor en este tipo particular de instancia que estoy presentando aquí, que si existen casos en los que uno es mejor que el otro, o viceversa.
M. Justin

Respuestas:

8

En términos de programación funcional, lo que usted y su colega están discutiendo es el estilo libre de puntos , más específicamente la reducción de eta . Considere las siguientes dos tareas:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Estos son operacionalmente equivalentes, y el primero se llama estilo significativo mientras que el segundo es estilo libre de puntos . El término "punto" se refiere a un argumento de función con nombre (esto proviene de la topología) que falta en el último caso.

En términos más generales, para todas las funciones F, una envoltura lambda que toma todos los argumentos dados y los pasa a F sin cambios, luego devuelve el resultado de F sin cambios es idéntico a F en sí. La eliminación de lambda y el uso de F directamente (pasando del estilo significativo al estilo sin puntos anterior) se denomina reducción eta , un término que se deriva del cálculo lambda .

Hay expresiones sin puntos que no se crean mediante la reducción de eta, el ejemplo más clásico es la composición de funciones . Dada una función de tipo A a tipo B y una función de tipo B a tipo C, podemos componerlas juntas en una función de tipo A a tipo C:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Ahora supongamos que tenemos un método Result deriveResult(Foo foo). Como un predicado es una función, podemos construir un nuevo predicado que primero llama deriveResulty luego prueba el resultado derivado:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Aunque la implementación de composeusos de estilo significativo con lambdas, el uso de componer para definir no isFoosResultInFuturetiene puntos porque no es necesario mencionar argumentos.

La programación libre de puntos también se llama programación tácita , y puede ser una herramienta poderosa para aumentar la legibilidad al eliminar detalles sin sentido (juego de palabras) de una definición de función. Java 8 no es compatible con la programación libre de puntos casi tan a fondo como los lenguajes más funcionales como Haskell, pero una buena regla general es realizar siempre la reducción de eta. No es necesario usar lambdas que no tengan un comportamiento diferente de las funciones que envuelven.

Jack
fuente
1
Creo que lo que estamos discutiendo mi colega y yo es más sobre estas dos tareas: Predicate<Result> pred = this::isResultInFuture;y Predicate<Result> pred = resultInFuture();donde resultInFuture () devuelve a Predicate<Result>. Entonces, si bien esta es una gran explicación de cuándo usar la referencia de método frente a una expresión lambda, no cubre este tercer caso.
M. Justin
44
@ M.Justin: La resultInFuture();asignación es idéntica a la asignación lambda que presenté, excepto en mi caso su resultInFuture()método está en línea. De modo que ese enfoque tiene dos capas de indirección (ninguna de las cuales hace nada): el método de construcción lambda y la propia lambda. ¡Peor aún!
Jack
5

Ninguna de las respuestas actuales en realidad aborda el núcleo de la pregunta, que es si la clase debería tener un private boolean isResultInFuture(Result result)método o un private Predicate<Result> resultInFuture()método. Si bien son en gran medida equivalentes, hay ventajas y desventajas para cada uno.

El primer enfoque es más natural si llama al método directamente además de crear una referencia de método. Por ejemplo, si prueba este método por unidad, podría ser más fácil usar el primer enfoque. Otra ventaja del primer enfoque es que el método no tiene que preocuparse por cómo se usa, desacoplándolo de su sitio de llamada. Si luego cambia la clase para que llame al método directamente, este desacoplamiento dará sus frutos de una manera muy pequeña.

El primer enfoque también es superior si desea adaptar este método a diferentes interfaces funcionales o diferentes uniones de interfaces. Por ejemplo, es posible que desee que la referencia del método sea de tipo Predicate<Result> & Serializable, que el segundo enfoque no le brinda la flexibilidad para lograrlo.

Por otro lado, el segundo enfoque es más natural si tiene la intención de realizar operaciones de orden superior en el predicado. Predicate tiene varios métodos que no se pueden invocar directamente en una referencia de método. Si tiene la intención de utilizar estos métodos, el segundo enfoque es superior.

En definitiva, la decisión es subjetiva. Pero si no tiene la intención de llamar a métodos en Predicate, me inclinaría por preferir el primer enfoque.

Reinstalar a Mónica
fuente
Las operaciones de orden superior en el predicado todavía están disponibles al emitir la referencia del método:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack
4

Ya no escribo código Java, pero escribo en un lenguaje funcional para vivir, y también apoyo a otros equipos que están aprendiendo programación funcional. Las lambdas tienen un delicado punto dulce de legibilidad. El estilo generalmente aceptado para ellos es que los usa cuando puede incorporarlos, pero si siente la necesidad de asignarlos a una variable para usarlos más tarde, probablemente debería definir un método.

Sí, las personas asignan lambdas a variables en tutoriales todo el tiempo. Esos son solo tutoriales para ayudarlo a comprender a fondo cómo funcionan. En realidad, no se usan de esa manera en la práctica, excepto en circunstancias relativamente raras (como elegir entre dos lambdas usando una expresión if).

Eso significa que si su lambda es lo suficientemente corta como para ser fácilmente legible después de la alineación, como el ejemplo del comentario de @JimmyJames, entonces hágalo:

people = sort(people, (a,b) -> b.age() - a.age());

Compare esto con la legibilidad cuando intente incorporar su lambda de ejemplo:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

Para su ejemplo particular, personalmente lo marcaría en una solicitud de extracción y le pediría que lo extraiga a un método con nombre debido a su longitud. Java es un lenguaje fastidiosamente detallado, por lo que quizás con el tiempo sus propias prácticas específicas del idioma diferirán de otros lenguajes, pero hasta entonces, mantenga sus lambdas cortas e integradas.

Karl Bielefeldt
fuente
2
Re: verbosidad: si bien las nuevas adiciones funcionales no lo hacen, por sí mismas reducen mucha verbosidad, permiten formas de hacerlo. Por ejemplo, puede definir: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)con la implementación obvia y luego puede escribir código como este: lo people = sort(people, (a,b) -> b.age() - a.age());cual es una gran mejora de la OMI.
JimmyJames el
Ese es un gran ejemplo de lo que estaba hablando, @JimmyJames. Cuando las lambdas son cortas y pueden estar en línea, son una gran mejora. Cuando se alargan tanto que quieres separarlos y darles un nombre, es mejor que solo definas un método. Gracias por ayudarme a comprender cómo se interpretó mal mi respuesta.
Karl Bielefeldt el
Creo que tu respuesta estuvo bien. No estoy seguro de por qué sería rechazado.
JimmyJames el