Tengo dificultades para encontrar recursos sobre cómo escribir programas en un estilo funcional. El tema más avanzado que pude encontrar discutido en línea fue usar la tipificación estructural para reducir las jerarquías de clase; la mayoría solo trata de cómo usar map / fold / reduce / etc para reemplazar los bucles imperativos.
Lo que realmente me gustaría encontrar es una discusión en profundidad de una implementación OOP de un programa no trivial, sus limitaciones y cómo refactorizarlo en un estilo funcional. No solo un algoritmo o una estructura de datos, sino algo con varios roles y aspectos diferentes, quizás un videojuego. Por cierto, leí la Programación funcional del mundo real de Tomas Petricek, pero me quedo con ganas de más.
Respuestas:
Definición de programación funcional
La introducción a La alegría de Clojure dice lo siguiente:
Programación en Scala 2nd Edition p. 10 tiene la siguiente definición:
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:
Funcional (usando una función anónima o "lambda" en Scala):
Versión Scala más azucarada:
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:
Luego, la clase Bus proporciona un iterador interno:
Finalmente, pasa un objeto de función anónimo al Bus:
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:
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:
Luego en autobús:
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:
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 .
fuente
Tengo experiencia personal "logrando" esto. Al final, no se me ocurrió algo que sea puramente funcional, pero se me ocurrió algo con lo que estoy feliz. Así es como lo hice:
x
, hágalo para que se pase el método enx
lugar de llamarthis.x
.x.methodThatModifiesTheFooVar()
enfooFn(x.foo)
map
,reduce
,filter
, etc.No pude deshacerme del estado mutable. Era demasiado poco idiomático en mi lenguaje (JavaScript). Pero, al hacer que todo el estado pase y / o regrese, todas las funciones son posibles de probar. Esto es diferente de OOP, donde configurar el estado llevaría demasiado tiempo o separar dependencias a menudo requiere modificar primero el código de producción.
Además, podría estar equivocado acerca de la definición, pero creo que mis funciones son referencialmente transparentes: mis funciones tendrán el mismo efecto con la misma entrada.
Editar
Como puede ver aquí , no es posible crear un objeto verdaderamente inmutable en JavaScript. Si eres diligente y controlas quién llama tu código, puedes hacerlo creando siempre un nuevo objeto en lugar de mutar el actual. No valió la pena el esfuerzo para mí.
Pero si está utilizando Java , puede usar estas técnicas para hacer que sus clases sean inmutables.
fuente
No creo que sea realmente posible refactorizar el programa por completo; tendrías que rediseñar y reimplementar en el paradigma correcto.
He visto la refactorización de código definida como una "técnica disciplinada para reestructurar un cuerpo de código existente, alterando su estructura interna sin cambiar su comportamiento externo".
Podría hacer que ciertas cosas sean más funcionales, pero en esencia todavía tiene un programa orientado a objetos. No puede simplemente cambiar pequeños fragmentos para adaptarlo a un paradigma diferente.
fuente
Creo que esta serie de artículos es exactamente lo que quieres:
Retrojuegos puramente funcionales
http://prog21.dadgum.com/23.html Parte 1
http://prog21.dadgum.com/24.html Parte 2
http://prog21.dadgum.com/25.html Parte 3
http://prog21.dadgum.com/26.html Parte 4
http://prog21.dadgum.com/37.html Seguimiento
El resumen es:
El autor sugiere un ciclo principal con efectos secundarios (los efectos secundarios deben ocurrir en algún lugar, ¿verdad?) Y la mayoría de las funciones devuelven pequeños registros inmutables que detallan cómo cambiaron el estado del juego.
Por supuesto, al escribir un programa del mundo real, mezclará y combinará varios estilos de programación, utilizando cada uno donde sea más útil. Sin embargo, es una buena experiencia de aprendizaje intentar escribir un programa de la manera más funcional / inmutable y también escribirlo de la manera más espagueti, utilizando solo variables globales :-) (hágalo como un experimento, no en producción, por favor)
fuente
Probablemente tenga que cambiar todo su código al revés ya que OOP y FP tienen dos enfoques opuestos para organizar el código.
OOP organiza el código alrededor de los tipos (clases): diferentes clases pueden implementar la misma operación (un método con la misma firma). Como resultado, OOP es más apropiado cuando el conjunto de operaciones no cambia mucho, mientras que se pueden agregar nuevos tipos muy a menudo. Por ejemplo, considere una biblioteca GUI en el que cada widget tiene un conjunto fijo de métodos (
hide()
,show()
,paint()
,move()
, y así sucesivamente), pero nuevos widgets podrían añadirse como la biblioteca se extiende. En OOP es fácil agregar un nuevo tipo (para una interfaz dada): solo necesita agregar una nueva clase e implementar todos sus métodos (cambio de código local). Por otro lado, agregar una nueva operación (método) a una interfaz puede requerir cambiar todas las clases que implementan esa interfaz (aunque la herencia puede reducir la cantidad de trabajo).FP organiza el código en torno a operaciones (funciones): cada función implementa alguna operación que puede tratar diferentes tipos de diferentes maneras. Esto generalmente se logra enviando el tipo a través de la coincidencia de patrones o algún otro mecanismo. Como consecuencia, FP es más apropiado cuando el conjunto de tipos es estable y se agregan nuevas operaciones con mayor frecuencia. Tomemos, por ejemplo, un conjunto fijo de formatos de imagen (GIF, JPEG, etc.) y algunos algoritmos que desea implementar. Cada algoritmo puede implementarse mediante una función que se comporta de manera diferente según el tipo de imagen. Agregar un nuevo algoritmo es fácil porque solo necesita implementar una nueva función (cambio de código local). Agregar un nuevo formato (tipo) requiere modificar todas las funciones que ha implementado hasta ahora para admitirlo (cambio no local).
En pocas palabras: OOP y FP son fundamentalmente diferentes en la forma en que organizan el código, y cambiar un diseño de OOP en un diseño de FP implicaría cambiar todo su código para reflejar esto. Sin embargo, puede ser un ejercicio interesante. Véanse también estas notas de lectura del libro SICP citadas por mikemay, en particular las diapositivas 13.1.5 a 13.1.10.
fuente