Tras décadas de uso comparativamente minoritario respecto de los lenguajes imperativos, de un tiempo a esta parte el estilo de programación funcional está ganando popularidad entre los programadores profesionales. A diferencia de en ocasiones anteriores, cuando se crearon lenguajes específicamente funcionales, esta vez la tendencia es a introducir la programación funcional en lenguajes tradicionalmente imperativos para facilitar la transición. Voy a intentar escribir un post lo menos técnico posible, aún así es un post sobre programación para cuyos interesados no técnicos debo pedir un poco de paciencia y curiosidad en su lectura. Los más conocedores de la materia pueden saltarse la introducción ¿Qué es la programación funcional? y pasar directamente a la parte de Nuevos usos y lenguajes.
¿Qué es la programación funcional?
La primera referencia importante al paradigma funcional se encuentra en la presentación que John Backus (la B en la Backus-Naur-Form y creador de Fortran) dio al recibir el premio Turing en 1977 titulada Can programming be liberated from the von Neumann style? brilantemente comentada por Dijkstra en A review of the 1977 Turing Award Lecture. Ambos artículos son largos y técnicos pero están redactados de manera se pueden entender bastante bien incluso sin ser programador y, en todo caso, deberían ser de lectura obligatoria para cualquier persona que aspire a tener formación académica en programación.
El primer ejemplo de Backus para explicar lo que es la programación funcional se encuentra en su especificación del producto escalar de dos vectores a y b de dimensión d.
c = 0
for i = 1 to d
c = c + ai × bi
Las críticas de Backus a la forma anterior de calcular un producto escalar son:
1. En vez de lo que se quiere hacer, el algoritmo explica cómo hacerlo, ergo para entenderlo es necesario “ejecutarlo”.
2. Se trata de un bucle diseñado para una máquina de von Neumann que lee y escribe una palabra en cada ciclo entre la CPU y la memoria, creando un cuello de botella en dicho canal de comunicación.
3. Los lenguajes imperativos carecen de propiedades útiles para trabajar matemáticamente con ellos.
4. Existe una separación entre las sentencias del lenguaje (en azul) y lo que serían las librerías externas, creando dos mundos separados, el del nucleo del lenguaje por un lado y el de las funciones definidas por el programador por otro.
La especificación alternativa del mismo algoritmo que Backus propone para solucionar los problemas anteriores es:
def InnerProduct ≡ (Insert +) ∘ (ApplyToAll ×) ∘ Transpose
Si no has entendido nada de la línea anterior no te preocupes, ese es el primer problema de la programación funcional: que al tratarse de un intento de reducir la programación a matemáticas o estás previamente familiarizado con las matemáticas o es imposible enterarse de nada leyendo el código (el PDF con la charla de Backus enlazado más arriba explica esta definición de InnerProduct con detalle).
En un lenguaje moderno como Scala escribiríamos:
def InnerProduct(a:List[Double], b:List[Double]) = a zip b map Function.tupled(_ * _) sum
Lo cual es un poco más intuitvo que la notación original de Backus pero aún lejos del bucle iterativo explícito.
La objeción principal de Dijkstra a la propuesta de Backus es que, aunque se especifique el algoritmo funcionalmente, después de todo sigue corriendo en una máquina de von Neumann con lo cual en algún momento habrá que convertir las funciones en bucles cambiando el problema original simplemente por otro de escribir compiladores especializados en optimizar la ejecución de especificaciones funcionales. Este argumento es cierto sólo en parte. Si bien la implementación de lenguajes funcionales tiene que resolver algunos problemas en su adaptación al hardware de un computador por otra parte abre posibilidades de optimización basadas en programación concurrente y la evaluación perezosa de funciones. En el ejemplo anterior, no se puede paralelizar directamente el bucle imperativo porque cada paso depende del resultado del anterior, pero sí es posible pensar con facilidad en la manera de ejecutar en paralelo las funciones que multipican cada par de elementos del vector. Hoy en día se considera que una de las características esenciales para que un lenguaje sea fácilmente paralelizable es que los objetos que maneja sean inmutables, es decir, que el estado o valor del objeto no pueda ser modificado una vez establecido. Esto es debido a que si el estado de un objeto puede cambiar en cualquier momento entonces el compilador no puede hacer optimizaciones basadas en la certeza de que un objeto no va a cambiar inadvertidamente.
Para no perderse demasiado en detalles técnicos, lo esencial que distingue la programación funcional de la imperativa y la orientada a objeto es que en programación funcional las funciones pueden pasarse como parámetros a otras funciones. Además de valores (u objetos) las funciones pueden devolver otras funciones como resultado.
Mi opinión personal es que dotar a la programación de sólidos fundamentos matemáticos es bueno, pero no se puede reducir la programación a matemáticas. Prácticamente todos los ejemplos de progrmación funcional que se pueden encontrar versan sobre cálculos del estilo de determinar si un número es primo o hallar su factorial. Pero los programas reales presentan muchísimos más desafíos diferentes. Es como la relación entre la física cuántica teoríca y los aceleradores de hadrones. Es necesario empezar con algún modelo para las partículas, pero para verificar el modelo y elevarlo al grado de teoría se necesita construir un acelerador, y para construir el acelerador es preciso conocer muchísmas cosas que no tienen nada que ver con física de partículas tales como la forma práctica de mover ese electroimán de 5.000Kg desde aquí hasta hallá, cómo aislar los ordenadores de los campos magnéticos producidos por el imán, de dónde sacar la electricidad para hacerlo funcionar, etc. Al final se acaba volviendo al dilema que planteaba Backus: o bien usas un lenguaje mastodónticamente grande en su definición o bien empiezas con un lenguaje funcional muy pequeño y le añades un montón de librerías de servicio. A la postre la complejidad neta es la misma.
El primer lenguaje ampliamente utilizado para programación funcional fue Lisp, en las universidades frecuentemente se usa Haskell para enseñar programación funcional y comercialmente se utilizan técnicas de programación funcional en muchos lenguajes que pasaré a comentar a continuación.
Nuevos usos y lenguajes
Un buen sitio para empezar a experimentar técnicas de programación funcional es JavaScript o Python. Ambos son lenguajes interpretados multiparadigma con tipado dinámico que permiten pasar funciones como parámetro y ámbos son relativamente fáciles de aprender (al menos lo básico).
Probablemente el mayor empujón reciente a la programación funcional se lo ha dado Scala. La repercusión de Scala en la comunidad Java ha sido tan grande que la mayor novedad en Java 8 son características de programación funcional que siguen en todo lo posible la forma de hacer las cosas en Scala. Desde mi punto de vista, lo más potente ahora mismo para programación funcional es Scala por cuatro motivos:
1. Scala corre sobre la máquina virtual de Java (JVM). Es compilado, no interpretado. Puede reutilizar todas las librerías Java pre-existentes y sólo con un poquito más de dificultad, también se puede llamar a código Scala desde Java. Existe también una implementación de Python para JVM (Jython) pero el entorno natural de Python no es ese y personalmente no conozco a nadie que use Jython en producción.
2. Además de la programación funcional, Scala incluye una forma de herencia múltiple similar a la de C++
3. La librería Akka para programación concurrente con actores soporta muy bien Scala y es muy potente. Existen librerías para programación concurrente en Python, pero, que yo sepa, en muchos casos todavía hay que manejar los hilos a mano y hay que irse a Erlang para encontrar un modelo de computación distribuida que sea tan seguro de utilizar como Akka.
4. Los programas Scala son más cortos, elegantes y fácilmente mantenibles que sus homólogos Java. En muchos aspectos Scala se parece a Python aunque incluye una mayor cantidad de funcionalidades de serie.
Las principales pegas que yo le encuentro a Scala son que existen demasiadas formas diferentes de escribir lo mismo y que el sistema de tipos y herencia es posiblemente demasiado académico y complejo. Se nota que su creador Martin Odersky quería crear un lenguaje sintácticamente diferente de Java pero que fuese fácil de adoptar por los programadores Java. Y por ello existen dos estilos de Scala: «Scala Puro» y «Scala Java» y leyendo ambos dialectos a duras penas nadie diría que son el mismo lenguaje de programación. Por otra parte, la posibilidad de sobreescribir y sobrecargar los operadores, como se hacía en C++, y lindeces como los implicits, las curried functions o las monads permiten escribir programas que son completamente indescifrables a simple vista.
En un intento de dotar a Scala de una mayor versatilidad han aparecido frameworks de propósito específico, el más notable de todos Scala Play, que es a Scala lo mismo que Django es a Python y que a mi no me gusta exactamente por la misma razón: porque proporcionan una forma rápida de hacer aplicaciones web de una determinada manera (que no está mal) pero esa no es mi manera.
En competencia con Scala se encuentra Clojure, un dialecto de Lisp creado por Google. A mi Clojure me sabe demasiado a Lisp, y nunca me he apañado bien con los infinitos paréntesis anidados de Lisp. Las ventajas de Clojure son que algunos aspectos de programación concurrente están muy elegantemente resueltos y que puede correr tanto sobre JVM como sobre CLR o JS. Es decir, en principio se puede escribir un programa en Clojure y ejecutarlo sobre Java, sobre Microsoft .NET o sobre un navegador con JavaScript.
Para las aplicaciones de programación concurrente distribuida es posible plantearse el uso de Erlang. Erlang necesita su propia máquina virtual (BEAM) y sus propias librerías (OTP).
Se puede también hacer programación funcional en Ruby. Los métodos no se pueden pasar como parámetro en Ruby, pero existe otro elemento del lenguaje, el block, que es un trozo arbitrario de código que se puede pasar de un lado a otro.
Es posible encontrar, por supuesto, otros lenguajes como Scheme, OCaml o F# que no voy a tratar aqui.
En conclusión, la meta es crear programas más cortos, fáciles de entender y eficientes sobre hardware multinúcleo. Pero la programación funcional no hace magia. Es posible escribir código funcional dificilísimo de entender y que se ejecuta con una lentitud y consumo de recursos apabullante. Paralelizar todo no es siempre la solución ideal, de hecho, los artículos más recientes sobre escalabilidad insisten una y otra vez en que es mejor intentar reducir el número de hilos al mínimo para evitar el cambio de contexto que invalida los cache y, por consiguiente, genera el cuello de botella de Backus entre el procesador y la memoria.
«Paralelizar todo no es siempre la solución ideal, de hecho, los artículos más recientes sobre escalabilidad insisten una y otra vez en que es mejor intentar reducir el número de hilos al mínimo para evitar el cambio de contexto que invalida los cache y, por consiguiente, genera el cuello de botella de Backus entre el procesador y la memoria.»
Podrías poner los links a dichos artículos
pd: interesante artículo
Mira por ejemplo 12 Ways to Increase Throughput by 32X and Reduce Latency by 20X