Como desarrollador de software, el manejo de la complejidad es un desafío constante. Tal complejidad puede originarse de muchos factores, incluyendo los cambiantes requerimientos de negocio, junto con la necesidad de la escalabilidad y fiabilidad del software. A lo largo de los años, se han desarrollado diferentes paradigmas y metodologías de programación. Uno de ellos es la programación funcional, un enfoque que puede mejorar significativamente la manera en que los desarrolladores manejan la complejidad del software.
¿Qué es la programación funcional?
La programación funcional o functional programming (FP) es un poderoso paradigma de programación declarativo que puede mejorar considerablemente la manera en que los desarrolladores abordan la resolución de problemas [1]. En esencia, FP trata la computación como la evaluación de expresiones matemáticas en lugar de como una secuencia de sentencias imperativas [2]. En este paradigma, las funciones se consideran de primera clase, capaces de ser creadas, almacenadas y modificadas como cualquier otra variable. Esta flexibilidad permite altos niveles de abstracción, modularización y predictibilidad, dando como resultado programas que reflejan más precisamente el comportamiento previsto de un sistema. En consecuencia, FP facilita a los desarrolladores la producción de código claro, conciso y reutilizable, mejorando así la estabilidad y mantenibilidad de los sistemas de software [3].
En FP se enfatiza el uso de funciones puras, las cuales producen la misma salida para una entrada dada y no tienen efectos secundarios. El comportamiento de las funciones es más fácil de entender, debido a que no depende de un estado externo o de datos mutables. Las estructuras de datos se tratan como no modificables una vez que se crean, lo que conduce a un código más seguro para hilos de ejecución, y menos propenso a errores relacionados con el estado compartido [4].
Adicionalmente, FP permite a los desarrolladores construir funcionalidades complejas combinando funciones simples. Esta capacidad de composición promueve la reutilización de código y permite la creación de pipelines donde los datos fluyen a través de una serie de transformaciones, cada una manejada por una función separada. Al descomponer problemas en partes más pequeñas, los desarrolladores pueden elaborar código modular y altamente testeable, que es más fácil de depurar y mantener a largo plazo [5].
Pensamiento funcional
En FP es crucial distinguir entre acciones, cálculos y datos. Este modo de pensar puede ayudar a los desarrolladores a entender qué partes de su código son simples de manejar y qué partes requieren más atención [1].
Las acciones son operaciones que producen algún efecto secundario, como modificar una variable en el ámbito externo, guardar cambios en una base de datos o realizar una solicitud HTTP. Dependiendo de cuándo se ejecuten y cuántas veces se ejecuten, pueden producir resultados diferentes según su contexto de ejecución.
Los cálculos son funciones puras que solo transforman entradas en una salida, sin causar efectos secundarios. Se pueden ejecutar en cualquier momento y muchas veces, siempre produciendo la misma salida para las mismas entradas.
Los datos representan hechos sobre eventos. Codifican significado a través de su estructura alineada con el dominio del problema. Los datos se utilizan para realizar cálculos, y los resultados de estos cálculos luego sirven como entradas para las acciones.
A continuación, se tiene un ejemplo que ilustra estos conceptos. Un carro de compras puede involucrar una acción para agregar un ítem al carro, un cálculo que obtiene el precio total, y datos que representan los ítems del carro. Estos aspectos estarían relacionados del siguiente modo:
Agregar al carro: Acción que cambia el estado del carro de compras al incluir un nuevo producto. Ejecutar "Agregar al carro" muchas veces, o en diferentes contextos de ejecución produciría un resultado diferente.
Obtener precio total: Cálculo que recibe los ítems del carro como entrada para obtener el precio total. Está garantizado que ejecutar este cálculo produce el mismo resultado para los mismos ítems del carro.
Ítems del carro: Datos que representan qué ítems están en el carro.
En el siguiente diagrama de línea temporal se describen las interacciones entre los aspectos anteriores. Se agregaron acciones y datos adicionales para mejorar el ejemplo.
Diagrama de línea temporal: Ejemplo de carro de compras.
El diagrama de línea temporal es una herramienta valiosa para visualizar la interacción entre acciones, cálculos y datos a lo largo del tiempo. Esto facilita la comprensión de procesos complejos, asíncronos o paralelos. Además, un diagrama de línea temporal puede ser descompuesto o agregado en varios niveles de detalle dependiendo del problema que se esté resolviendo [1].
Refactorización
La refactorización de acciones a cálculos es una práctica común en la programación funcional para mejorar la testeabilidad, predictibilidad y simplicidad del código [6][7]. Consideremos el ejemplo anterior del carro de compras y un procedimiento de refactorización:
Aislamiento: Se identifican y separan las partes del código responsables de efectos secundarios de aquellas que realizan cálculos. En el contexto de un carro de compras, el aislamiento se centraría en identificar dónde el código modifica variables globales (como actualizar el total cuando se agrega un ítem al carro), o interactúa con sistemas externos (como verificaciones de inventario). El objetivo es delinear claramente estos efectos secundarios.
Ejemplo: Si el precio total del carro de compras se actualiza globalmente cada vez que se agrega un ítem, es necesario identificar y aislar este proceso de actualización del resto de la lógica del carro.
Extracción: Una vez que los efectos secundarios están aislados, el siguiente paso es extraerlos en funciones separadas. Esto significa mover el código que produce efectos secundarios aparte del código que no los produce, convirtiendo las acciones en funciones puras cuando sea posible.
Ejemplo: A partir del proceso de actualización ya aislado, es necesario extraer la lógica que calcula el precio total en una función separada. En lugar de tener una función que actualice el total global y calcule el nuevo total, se crea una función exclusivamente para calcular el total basado en los ítems en el carro. Esta función tomaría los contenidos del carro como entrada y devolvería el costo total, sin modificar ningún estado global.
Reemplazo: El último paso consiste en sustituir las dependencias implícitas y los efectos secundarios en el código por entradas y salidas explícitas. Esta refactorización tiene por objetivo hacer que las funciones sean puras, donde su salida depende únicamente de su entrada y no del estado externo.
Ejemplo: La función que calcula el precio total se modifica para que ya no utilice ni cambie directamente ninguna variable global. En su lugar, la función acepta los contenidos del carro como argumento y devuelve el precio total. Cualquier interacción con el estado del carro debe hacerse fuera de esta función, asegurando que realice un cálculo predecible sin efectos secundarios.
Al aplicar estas técnicas de refactorización al sistema del carro de compras, el código se vuelve más limpio, modular y fácil de probar. Cada función tiene un propósito claro, los efectos secundarios se gestionan deliberadamente, y se mejora la predictibilidad y mantenibilidad de la solución.
Conclusión
Adoptar un enfoque de programación funcional no se trata simplemente de aprender un conjunto de herramientas y técnicas; es un cambio de perspectiva hacia una manera estructurada de pensar sobre el desarrollo de software. A medida que las acciones se separan de los cálculos, los datos pueden ser manejados de manera predecible. Además, herramientas como los diagramas de línea temporal pueden ayudar a diseñar una solución visualmente. De esta manera, se facilita la producción de código de alta calidad.
En consecuencia, la programación funcional se convierte en un conjunto de habilidades y conceptos que permiten construir y gestionar la complejidad del software de manera más efectiva. Esto es especialmente cierto en el mundo actual de sistemas distribuidos e interfaces de usuario sofisticadas. El pensamiento funcional promete mejorar no solo la calidad del código, sino también el nivel de habilidad general de un desarrollador de software.
Referencias
[1] E. Normand, “Grokking Simplicity: Taming Complex Software with Functional Thinking,” Simon and Schuster, 2021.
[2] K. Davis and J. Hughes, “Functional Programming,” pp. 2, 1990, doi: 10.1007/978-1-4471-3166-3
[3] C. Oliveira, “Functional Programming Techniques,” in Options and Derivatives Programming in C, Berkeley, CA: Apress, 2016, pp. 127-142.
[4] S. Höck and R. Riedl, “chemf: A purely functional chemistry toolkit,” Journal of Cheminformatics, vol. 4, no. 1, pp. 6, 2012, doi: 10.1186/1758-2946-4-38
[5] J. Hughes, “Why Functional Programming Matters,” The Computer Journal, vol. 32, no. 2, pp. 98-107, 1989, doi: 10.1093/comjnl/32.2.98
[6] S. Thompson and H. Li, “Refactoring tools for functional languages,” Journal of Functional Programming, vol. 23, no. 3, pp. 293-350, 2013, doi: 10.1017/s0956796813000117
[7] J. Gibbons, “Calculating Functional Programs,” in Lecture Notes in Computer Science: Algebraic and Coalgebraic Methods in the Mathematics of Program Construction, Berlin, Heidelberg: Springer Berlin Heidelberg, 2002, pp. 151-203.
Commentaires