Programación imperativa vs declarativa IV (paradigma funcional)

Programación imperativa vs declarativa IV (paradigma funcional)
Facebook Twitter Flipboard E-mail

Bucking horse, así es la programación funcional hoy en día

Largo y tortuoso ha sido el camino que nos lleva hasta la meta. Hemos contrastado una especificación de lenguaje imperativa con una declarativa, hemos entendido las estrategias óptimas de cálculo simbólico y hemos intuido el prometedor futuro de la verificación automática de teoremas (y arañado sutilmente la demostración automática de teoremas).

¿Para qué?. Por amena y relajante que haya sido la lectura (risas) y por mucho que los matemáticos digan que no hace falta que algo tenga utilidad para secarte el cerebro pensando en ello, a nosotros los programadores lo que realmente nos importa es la aplicación práctica del caso.

El corolario final sobre el que rumiaremos en esta entrada, es si (como parece) la programación funcional va a jugar, a corto/medio plazo, un papel importante en el desarrollo de software (en general) y si merece la pena que dediquemos nuestro preciado tiempo a maltratar nuestras neuronas, cuando podrían estar componiendo, tranquilamente, sinfonías con PHP. ¿Te atreves?.


¿Qué es qué?

Cuando contrastábamos la programación imperativa con la declarativa, utilicé deliberadamente el concepto “contexto de un lenguaje” para poner de manifiesto que la “declaratividad” de un lenguaje es subjetiva (no hay una definición libre de ambigüedades) y que por tanto un lenguaje es declarativo, en la medida que abstrae, absorbe, asimila, … la esencia de determinados procesos y nos libera de ellos.

El “contexto declarativo” de SQL (que puse como ejemplo) nos libera de tener que pensar y optimizar los procesos relacionados con el tratamiento eficiente de grandes volúmenes de información entre muchas fuentes diferentes (hash, árboles, buffers, planes de ejecución de consulta, etc…). De lo que no nos libera, es de saber qué datos y cómo debemos fusionarlos (hacer producto cartesiano de las tablas EMPRESA y EMPLEADO).

El “contexto declarativo” de un lenguaje imperativo (simplificándolo mucho) nos libera de tener que pensar en la mejor forma de trasladar las estructuras: if, while, for, … al código binario de una máquina. De lo que no nos libera, y esta es la cuestión, es de seguir pensando en términos de las instrucciones que suministra la máquina.

Así, entre SQL y (por ejemplo) C, hay una diferencia cualitativa muy importante, y es que el primero ha conseguido establecer un marco de trabajo que es independiente de la máquina, mientras que el segundo únicamente lo ha trasladado.

Por eso, aunque no haya una definición formal (sin ambigüedades y universalmente aceptada) de lo que es un lenguaje declarativo y de que existen muchos matices, no parece razonable argüir que C es un lenguaje declarativo.

Dicho lo anterior, vamos a intentar pensar en: qué es un lenguaje que adopta el paradigma funcional. Al final veremos, que si bien a muchos lenguajes se les puede poner la etiqueta de funcionales, sólo cuando éstos cumplen el requisito de “ser siempre y en todo momento” funcionales, pueden llevar con orgullo la etiqueta de “lenguaje funcional”.

La máquina impide el desarrollo teórico

Todos los lenguajes imperativos como C, han surgido para facilitar la transcripción de algoritmos diseñados para una máquina que posee unas características físicas concretas. Si la máquina en lugar de ser un procesador de los que conocemos hubiera sido otra máquina con principios físicos diferentes, es muy poco probable que C (y similares) se hubieran diseñado igual. Parece razonable pensar que, cuanto más diferentes sean las máquinas, más diferente serán los lenguajes (imperativos) diseñados para programarlas.

Así, vemos que la mayoría de los algoritmos han sido diseñados pensando en este tipo de máquinas (“imperativas”). A lo largo de la historia, se han ido refinando las estrategias a la hora de escribir los programas, mejorando paso a paso las “rudas estrategias” (eg. un for o una función global) por otras más refinadas (eg. un foreach o un método privado).

El problema es que este refinamiento ha generado una monstruosa cantidad de estructuras del desarrollo de software (singletones, tipado dinámico, clausuras, hashes, polimorfismo, reflexión, miembros estáticos/dinámicos, uniones, inyección de dependencias, etc…) con las que, el realizar una formalización adecuada, y por tanto entender sus relaciones íntimas, se hace del todo imposible, impidiendo (por lo complejo del caso) encontrar soluciones completas (soluciones de Dios). Digamos que el desorden es tal, que poner orden se vuelve una meta titánica.

Queda patente por tanto, que el desarrollo de software actual toma sus raíces de estructuras que han sido pensadas para la máquina y no para los algoritmos (dado un problema, no buscamos su mejor solución teórica, sino aquella que en la práctica podemos aplicar a nuestra máquina).

Programación declarativa

Muchos lenguajes que pueden ser considerados declarativos (eg. LaTeX) han surgido espontáneamente al intentar abstraer las tareas básicas que realizan, sin embargo, no es sino con lenguajes como SQL que se aprecia lo fructífero de establecer un marco formal que permita entender las relaciones íntimas de las estructuras del lenguaje (eg. en SQL para poder generar planes de consulta isomorfos [equivalentes] pero más eficientes).

Existen unos cuantos modelos que formalizan el concepto de computación, el mas famoso supongo que debería ser la máquina de Turing, sin embargo, la máquina de Turing no deja de ser un “procesador imperativo” muy sencillo (una máquina de estados finita) que si bien permite entender perfectamente la forma en que se procesa la información, no aporta absolutamente nada acerca de la idea que subyace en el algoritmo.

Es decir, dentro de todos los modelos de computación que podamos encontrar, parece ser (hasta no tener una formalización no podemos asegurar nada, claro) que deberíamos centrarnos en aquellos que asimilan, absorben, etc… en mayor medida el concepto abstracto de computación.

Aunque no me resulta sencillo, intentaré poner un ejemplo: una máquina de estados finita (como la de Turing) va “saltando” de un estado A a otro B siguiendo un procedimiento muy sencillo y bien establecido, por ejemplo, si nos fijamos en el siguiente grafo de una máquina de estados muy sencilla:

Ejemplo de DFA

Como vemos, la máquina empieza en el estado S1, si de la entrada se obtiene un 1 sigue en el estado S1, pero si se obtiene un 0 entonces pasa al estado S2 y no volverá al S1 hasta que no se obtenga otro 0 y así sucesivamente.

Con máquinas de estados como la descrita, se pueden representar todo tipo de algoritmos (eg. el juego Crysis 3) pero no hay nada en la máquina de estados que aporte información sobre lo que significa el algoritmo que implementa, sólo lo ejecuta, nada más. Para estudiar el algoritmo, para entender sus relaciones, debemos acceder al “pseudocódigo” con el que alguien generó esa máquina de estados. Es por eso que resulta tan difícil en la práctica realizar ingeniería inversa de programas compilados, se puede estudiar el código de un virus, una parte concreta de la protección de un software, etc… pero requiere mucho esfuerzo y, en general, no es posible recuperar las intenciones de quien lo programó, sino sólo saber qué hace el código (de que estado a que estado salta).

Sin embargo, la programación declarativa intenta absorber la esencia de los procesos. Siguiendo con el ejemplo, supongamos que un lenguaje declarativo dice que las funciones F(x) y G(y) son tales que su composición es conmutativa, es decir, que tanto da hacer F(G(z)) que hacer G(F(z)). En ese caso, tenemos una información que nos sirve para cualquier función y dará igual quien y cómo la programen, nosotros podremos jugar con esa información.

¿Que para qué sirve saber eso?, supón que nosotros tenemos que hacer un programa que compute F(G(z)), pero resulta que computar G conlleva un esfuerzo muy importante (eg. 30 segundos), por otro lado, computar F es muy rápido y eficiente y, además, F nos indica en el 90% de los casos, que el computo final fallará y en el 10% que no fallará. Como sabemos que el proceso es conmutativo, podemos computar primero F y sólo en el 10% de los casos computaremos G, ¡con un ahorro en tiempo del 90%!. Pero lo más importante, es que esto sirve para cualquier par de funciones que cumplan la conmutatividad y por tanto nuestro compilador/intérprete/máquina podrá realizar esta importantísima optimización en cualquier programa.

¿Que mi ejemplo no es práctico?, supón que tenemos una función llamada CheckLength(s) que dada una cadena de caracteres, devuelve la cadena vacía si su longitud es mayor de 5 Kbytes, pero devuelve esa misma cadena si es inferior o igual a 5 Kbytes. Tenemos otra función ComputeHash(s) con un coste muy alto, que toma cada palabra de la cadena y busca en una base de datos secuencias determinadas (dada una condición) y que tienen la misma longitud que esa misma palabra. En estas condiciones, ¡CheckLength(ComputeHash(s)) es equivalente a ComputeHash(CheckLength(s))!, ¿qué compiladores de los que conoces serían capaces de conmutar el ineficiente código CheckLength(ComputeHash(s)) por el tremendamente eficiente ComputeHash(CheckLength(s))?.

Como en los anteriores artículos de la serie, volvemos a intuir el tremendo avance cualitativo que significa una formalización de lo que significa computación (algoritmo, etc…) frente a avances cuantitativos como optimizaciones a bajo nivel o ingeniosas estructuras sin relación íntima con las demás (eg. inyección de dependencias).

Programación funcional, lenguajes funcionales

El paradigma funcional, pretende aplicar un pensamiento matemático al problema del diseño de algoritmos (y desarrollo de software por extensión). Para ello, en lugar de pensar en términos de la máquina física, empieza preguntándose por lo más básico: ¿qué es una función?. Fueron Alonzo Church y Stephen Kleene, en la década de 1930, los que iniciaron el Cálculo lambda con independencia de ninguna máquina física concreta, analizando únicamente las propiedades de sus objetos de estudio (las funciones).

La programación funcional establece unas propiedades que deben cumplir los elementos del lenguaje (de similar forma a como yo establecí la conmutatividad en las funciones del ejemplo anterior), con esas propiedades, se construyen y obtienen interesantes resultados de carácter general.

¿Serán esas propiedades las deseables?, ¿serán esas propiedades las que permitan una formalización útil en la práctica y no solo en la teoría de la computación?, no lo sabemos.

“¿Cómo?”, te estarás diciendo, “después de tanta promesa, de capacidades infinitamente superiores a la programación imperativa ¿resulta que no sabemos a donde parará todo ésto?”.

Más lo siento yo, pero no se puede asegurar. En la historia de las matemáticas (y en física también), es muy frecuente, sobre todo en áreas jóvenes, que surjan varias teorías, que unas sean olvidadas en favor de otras que parecen las prometedoras y, con el tiempo, se retome una de aquellas desechadas.

La programación funcional, en la forma que está adoptando actualmente, es únicamente uno de tantos posibles enfoques que pueden estudiarse. Por ejemplo SQL no es funcional sino algebraico y Prolog tampoco es funcional sino lógico.

Ésta es la razón por la que desde el principio de la serie he centrado la atención en la programación declarativa, sin concretar nada más que debe permitirse (favorecerse) una formalización exhaustiva (matemática) del lenguaje.

Tal y como encabeza el post, la programación funcional (en particular y declarativa en general) es como un caballo indómito al que se trata de domar. Hay importantes avances (apuntamos sucintamente en la serie) que hacen pensar que se va por el buen camino, pero en la práctica, estamos bastante lejos de poder arar los campos con semejantes bestias.

Sin entrar a detallar las propiedades deseables en un lenguaje funcional, no basta con que un lenguaje las permita para que éste sea funcional, además, debe exigirlas. Siguiendo con el ejemplo anterior, si nuestros programas a veces cumplirán la conmutatividad y a veces no ¿cómo sacar partido de las teorías sobre conmutatividad de funciones?, simplemente no se puede. (Nota: si tenemos un lenguaje J al que puede indicarse con un pragma u otro mecanismo que sea estricto o no, entonces realmente tenemos dos lenguajes, el “J estricto” y el “J no estricto”).

Algo muy diferente es que los lenguajes imperativos, de igual forma que adoptan estrategias como la inyección de dependencias, adopten estrategias de los lenguajes funcionales ¡pero eso no hace que sean lenguajes funcionales!. De todos modos, tampoco vamos a dejar de aprovecharnos de una estrategia funcional porque mi lenguaje no lo sea. Por ejemplo, PHP es manifiestamente imperativo y para nada funcional, sin embargo, si tengo que convertir una cadena de caracteres en una tabla hash, puedo hacer algo como lo siguiente:

Dicha solución, tiene y cumple varias propiedades de la programación funcional (y obviamente ha sido inspirada en ella), pero a nadie se le ocurriría decir que PHP es funcional.

A falta de una definición formal, se dice que ciertos lenguajes admiten el paradigma funcional (javascript, python, scala, …) e incluso que algunos lenguajes son funcionales (scheme, clojure, ocaml, …) cuando realmente incumplen propiedades que deberían de cumplir.

Es por esto que a los lenguajes funcionales “de verdad” se les suele llamar “puros”.

Si quieres aprender los sabores y sinsabores de la programación funcional, es en estos últimos, los lenguajes funcionales puros, en los que debes centrarte.

Programación funcional, en la vida real

Como desarrollador, desaconsejaría totalmente el desarrollo de ningún sistema informático “normal” sobre lenguajes funcionales. Es muchísimo más barato utilizar cualquier otro lenguaje imperativo. No voy a entrar a discutir aquí (bastante largo está saliendo el post) los conocidísimos argumentos que esgrimen los pro-funcionales (eg. la famosa frase de Haskell “aprende Haskell por el bien de todos”), entre otras cosas, porque me parece bastante evidente para cualquiera que sepa lo que es el desarrollo de software fuera de una Universidad, el despropósito de elegir (por ejemplo) Haskell frente a Python (ya se, ya se, conoces mi acritud hacia Python).

Siento decirlo, pero hoy en día, los lenguajes funcionales tienen muchas de las desventajas de usar un lenguaje funcional y muy pocas de las ventajas de usar un lenguaje funcional. Quizás una de las más notables es la paralelización, que como promesa está muy bien (no entremos a detallar ahora), pero que en la práctica dista mucho de disponer del algoritmo de Dios (en la teoría sí, en la práctica ni de lejos).

Existen multitud de ejemplos en los que, para realizar una tarea realmente simple en un lenguaje imperativo, te encuentras con una versión todavía más simple en un lenguaje funcional, por ejemplo, implementar Quick Sort en Haskell resulta en:

La sorpresa viene cuando ves que no es eficiente y entonces debes llegar a intrincadas soluciones como la siguiente:

Es solo un ejemplo (código original tomado de GHC), no el mejor, seguro, pero si es corto y fácilmente entendible. Cuando quieres profundizar, te encuentras con que a cada problema debes realizar un sesudo razonamiento (¿será por eso lo de “razonando con Haskell”?) para aplicar la solución, con frecuencia con soluciones complejas y poco satisfactorias. A la postre, da igual si al final la mejor solución sigue siendo la de Haskell (o similar), la cuestión es que en el día a día, las soluciones deben poderse aplicar de forma inmediata, sin miramientos y de forma satisfactoria.

Hoy en día lenguajes como Haskell tienen su sitio (ya comentamos lo de esL4 por ejemplo) pero difícilmente en la vida real.

Sin embargo, como desarrollador también, opino firmemente que a un programador le falta un aspecto importante para “completarse” si no ha profundizado en menor o mayor medida en la programación funcional. Existen multitud de estrategias funcionales de las que uno puede sacar partido en el día a día.

Al final, igual será verdad la frase “¡aprende Haskell por el bien de todos!”.

En Genbeta Dev | Programación imperativa vs declarativa III (Demostración Automática de Teoremas) , Programación imperativa vs declarativa II (Cálculo simbólico), Programación imperativa vs declarativa I.
Más información | Cálculo lambda.
Más información | Programación funcional.

Comentarios cerrados
Inicio