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?
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.Respuestas:
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:
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:
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 llamaderiveResult
y luego prueba el resultado derivado:Aunque la implementación de
compose
usos de estilo significativo con lambdas, el uso de componer para definir noisFoosResultInFuture
tiene 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.
fuente
Predicate<Result> pred = this::isResultInFuture;
yPredicate<Result> pred = resultInFuture();
donde resultInFuture () devuelve aPredicate<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.resultInFuture();
asignación es idéntica a la asignación lambda que presenté, excepto en mi caso suresultInFuture()
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!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 unprivate 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.
fuente
((Predicate<Resutl>) this::isResultInFuture).whatever(...)
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:
Compare esto con la legibilidad cuando intente incorporar su lambda de ejemplo:
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.
fuente
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: lopeople = sort(people, (a,b) -> b.age() - a.age());
cual es una gran mejora de la OMI.