Definición de programación funcional
La introducción a La alegría de Clojure dice lo siguiente:
La programación funcional es uno de esos términos informáticos que tiene una definición amorfa. Si le pide a 100 programadores su definición, probablemente recibirá 100 respuestas diferentes ...
La programación funcional concierne y facilita la aplicación y composición de funciones ... Para que un lenguaje se considere funcional, su noción de función debe ser de primera clase. Las funciones de primera clase se pueden almacenar, pasar y devolver como cualquier otro dato. Más allá de este concepto central, [las definiciones de PF pueden incluir] pureza, inmutabilidad, recursividad, pereza y transparencia referencial.
Programación en Scala 2nd Edition p. 10 tiene la siguiente definición:
La programación funcional se guía por dos ideas principales. La primera idea es que las funciones son valores de primera clase ... Puede pasar funciones como argumentos a otras funciones, devolverlas como resultados de funciones o almacenarlas en variables ...
La segunda idea principal de la programación funcional es que las operaciones de un programa deben mapear los valores de entrada a los valores de salida en lugar de cambiar los datos en su lugar.
Si aceptamos la primera definición, entonces lo único que debe hacer para que su código sea "funcional" es cambiar sus bucles al revés. La segunda definición incluye la inmutabilidad.
Funciones de primera clase
Imagine que actualmente obtiene una Lista de Pasajeros de su objeto de Autobús e itera sobre ella disminuyendo la cuenta bancaria de cada pasajero por el monto de la tarifa del autobús. La forma funcional de realizar esta misma acción sería tener un método en Bus, quizás llamado forEachPassenger que tome la función de un argumento. Luego, el Autobús iteraría sobre sus pasajeros, sin embargo, eso se logra mejor y su código de cliente que cobra la tarifa del viaje se pondría en una función y se pasaría a cada Pasajero. Voila! Estás usando programación funcional.
Imperativo:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Funcional (usando una función anónima o "lambda" en Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Versión Scala más azucarada:
myBus = myBus.forEachPassenger(_.debit(fare))
Funciones no de primera clase
Si su idioma no admite funciones de primera clase, esto puede ponerse muy feo. En Java 7 o anterior, debe proporcionar una interfaz de "Objeto funcional" como esta:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Luego, la clase Bus proporciona un iterador interno:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Finalmente, pasa un objeto de función anónimo al Bus:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8 permite que las variables locales sean capturadas en el alcance de una función anónima, pero en versiones anteriores, cualquier varibales debe declararse final. Para evitar esto, es posible que deba crear una clase de contenedor MutableReference. Aquí hay una clase específica de entero que le permite agregar un contador de bucle al código anterior:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
Incluso con esta fealdad, a veces es beneficioso eliminar la lógica complicada y repetida de los bucles repartidos por todo el programa al proporcionar un iterador interno.
Esta fealdad se ha corregido en Java 8, pero el manejo de excepciones comprobadas dentro de una función de primera clase sigue siendo realmente feo y Java aún asume el supuesto de mutabilidad en todas sus colecciones. Lo que nos lleva a los otros objetivos a menudo asociados con FP:
Inmutabilidad
El artículo 13 de Josh Bloch es "Prefiere la inmutabilidad". A pesar de que la basura común habla de lo contrario, la POO se puede hacer con objetos inmutables, y hacerlo lo hace mucho mejor. Por ejemplo, String en Java es inmutable. StringBuffer, OTOH necesita ser mutable para construir una cadena inmutable. Algunas tareas, como trabajar con buffers requieren inherentemente mutabilidad.
Pureza
Cada función debe ser al menos memorable: si le da los mismos parámetros de entrada (y no debe tener ninguna entrada además de sus argumentos reales), debe producir la misma salida cada vez sin causar "efectos secundarios" como cambiar el estado global, realizando I / O, o lanzando excepciones.
Se ha dicho que en la Programación Funcional, "generalmente se requiere algo de maldad para realizar el trabajo". 100% de pureza generalmente no es el objetivo. Minimizar los efectos secundarios es.
Conclusión
Realmente, de todas las ideas anteriores, la inmutabilidad ha sido la mayor victoria individual en términos de aplicaciones prácticas para simplificar mi código, ya sea OOP o FP. Pasar funciones a iteradores es la segunda mayor victoria. La documentación de Java 8 Lambdas tiene la mejor explicación de por qué. La recursión es excelente para procesar árboles. La pereza te permite trabajar con infinitas colecciones.
Si le gusta la JVM, le recomiendo que eche un vistazo a Scala y Clojure. Ambas son interpretaciones perspicaces de la programación funcional. Scala es de tipo seguro con una sintaxis algo similar a C, aunque realmente tiene tanta sintaxis en común con Haskell como con C. Clojure no es de tipo seguro y es un Lisp. Recientemente publiqué una comparación de Java, Scala y Clojure con respecto a un problema de refactorización específico. La comparación de Logan Campbell con Game of Life incluye a Haskell y también escribió Clojure.
PD
Jimmy Hoffa señaló que mi clase de autobús es mutable. En lugar de arreglar el original, creo que esto demostrará exactamente el tipo de refactorización de esta pregunta. Esto se puede solucionar haciendo que cada método en Bus sea una fábrica para producir un nuevo Bus, cada método en Passenger una fábrica para producir un nuevo Pasajero. Por lo tanto, he agregado un tipo de retorno a todo lo que significa que copiaré la función java.util.function.Function de Java 8 en lugar de la interfaz de consumidor:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Luego en autobús:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
Finalmente, el objeto de función anónimo devuelve el estado modificado de las cosas (un nuevo autobús con nuevos pasajeros). Esto supone que p.debit () ahora devuelve un nuevo Pasajero inmutable con menos dinero que el original:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Con suerte, ahora puede tomar su propia decisión sobre qué tan funcional desea hacer su lenguaje imperativo y decidir si sería mejor rediseñar su proyecto utilizando un lenguaje funcional. En Scala o Clojure, las colecciones y otras API están diseñadas para facilitar la programación funcional. Ambos tienen muy buena interoperabilidad de Java, por lo que puede mezclar y combinar idiomas. De hecho, para la interoperabilidad de Java, Scala compila sus funciones de primera clase en clases anónimas que son casi compatibles con las interfaces funcionales Java 8. Puede leer sobre los detalles en la sección Scala in Depth. 1.3.2 .