Mi pequeña obsesión con la programación funcional (parte 02)

¿Y qué tienen de bueno estos lenguajes? Como escribe John Hughes en “Why Functional Programming Matters“, mientras el software se está volviendo más y más complejo, es cada vez más importante estructurarlo bien. Software bien estructurado es fácil de escribir, fácil de corregir y fácil de reutilizar. Los lenguajes convencionales (lenguajes imperativos) ponen trabas conceptuales a la forma de estructurar (o modularizar) el software. La programación funcional tira abajo esas trabas.
¿Y por qué? El cálculo Lambda fue diseñado para realizar cómputos de manera matemática, es decir, por medio de funciones. En los lenguajes funcionales, no existen las variables, se reemplazan por funciones. No existen los ciclos, se reemplazan por recursión. Y un montón de otras cosas extrañas a los que nos hemos acostumbrado a la forma imperativa.

Whoa! Y cómo es eso? En la programación funcional (o PF de ahora en más), las variables son simplemente “alias” o identificadores para una expresión. Son creadas y eliminadas dinámicamente de la memoria a medida que se necesitan, por el compilador o intérprete de turno. Es como si en Java, todas las variables fueran declaradas como final (o const en C++). Por lo tanto, si todas las variables son constantes, ya no se denominan variables sino símbolos.
¿Y cómo se hace para programar sin variables? En la programación imperativa se utilizan las variables para almacenar datos, o en definitiva estados. Recordemos que podemos definir un estado esencialmente como el conjunto de valores que tienen las variables de un sistema en un momento dado. A Church no le interesaban las variables, ya que podía conocer los estados por medio de las funciones que se estaban calculando en un momento dado y los parámetros que esas funciones tenían. Por lo tanto los estados son “identificados” por el conjunto de funciones en la pila de llamadas y sus parámetros. Si queremos cambiar el estado, llamamos a las funciones con otros parámetros y listo. Por ejemplo, imaginemos un programa para invertir una cadena en Java, utilizando las reglas de la PF (variables final):


String invertir (String str) {
/* si la cadena está vacía, entonces ya está invertida */
if (str.length == 0) {
return str;
}
/* sino creo una nueva cadena concatenando la inversion del resto de la
* cadena con el primer caracter al final
*/
else {
return invertir (str.substring(1, str.length)) + str.substring(0, 1);
}
}

A partir del uso de la recursión y sin utilizar variables, ha sido posible crear una rutina para invertir cualquier cadena. Los conocedores dirán que éste método requiere de más memoria y un montón de llamadas a subrutinas para realizar lo mismo que con un bucle. Es cierto, pero los creadores de lenguajes funcionales también tienen sus técnicas para solucionar ésto :) . La ventaja de este enfoque es que este método no necesita declararse como “synchronized”. Al no depender de valores de variables internas, no hay problemas de condiciones de carrera en caso de que sea llamado por dos objetos simultáneamente.

Aún así parece que no tiene sentido programar de esta manera ¿no?. Bueno tengo unas cuantas cosas para contarles:

Ya que todo símbolo es una “variable final”, no hay efectos de lado. Es una de las “máximas” de los lenguajes funcionales. No hay que preocuparse por valores que se modifican porque las utilizó otra función (u otra función tiene una variable con el mismo nombre y puede haber problemas en lenguajes con alcance dinámico). Puesto de otra manera: las funciones llamadas con los mismos parámetros siempre retornan los mismos valores, no importa el contexto. Por lo tanto es muchísimo más fácil de probar y corregir. Si una función cambia su resultado, es porque cambiaron sus parámetros. Simple. No hay que pensar en otra cosa. Por lo tanto para generar una unidad de prueba, simplemente hay que pensar en llamar a la función con los parámetros que representen los casos extremos y listo. Esto es particularmente conveniente a la hora de trabajar con aplicaciones concurrentes.

Por lo tanto también es más simple seguir o crear la traza de un programa y en consecuencia “debuggearlo”. Una simulación de un programa funcional requiere unicamente de especificar los parámetros correctos en los lugares indicados. No hay que preocuparse de el orden de llamadas. Si una función no devuelve el resultado esperado bajo ciertos parámetros, nunca lo hará. Y si funciona bien, siempre lo hará bajo los mismos parámetros. Por lo tanto, reproducir un error es muy simple. Y solucionarlo también. En PF el estado del programa es la pila de llamadas a funciones y los parámetros que se les han pasado. En la Programación Imperativa tienes que fijarte en el orden de llamadas, en las variables globales, en el estado del sistema, en las variables de otras funciones o el estado de otros objetos. Por ésos son útiles los debuggers que dan toda ésa información, que es en definitiva, el estado del sistema…..

Otra ventaja de la inexistencia de efectos de lado es que los programas están adaptados para concurrencia sin modificaciones (o con muy pocas). No hay que preocuparse por deadlocks o condiciones de carrera, porque no hay variables compartidas!. Veamos un ejemplo. Supongamos que tenemos el siguiente código:

...
funcion_compleja_1(argumento1, argumento2,....)
funcion_compleja_2(argumento1, argumento2,....)
....

En un lenguaje Imperativo es necesario que se ejecute funcion_compleja_1 completamente antes de pasar a la siguiente llamada, ya que es necesario conocer el estado en el que queda el sistema luego de la llamada a función_compleja_1 (sobre todo si funcion_compleja_1 altera una variable global). Otros lenguajes como Java solucionan ésto declarando synchronized al método. Pero ésto bloquea la rutina para que no pueda ser llamada “más de una vez al mismo tiempo”. Por lo que disminuye el rendimiento de la aplicación.
En un lenguaje funcional, el compilador (si posee la capacidad) se “da cuenta” de que ambas funciones requieren mucho proceso y podría destinar un hilo a cada una para que se ejecuten de manera paralela.

Por ejemplo, tenemos el lenguaje Erlang, que fue creado con el propósito de desarrollar aplicaciones distribuidas, concurrentes y robustas. Ya las palabras “distribuidas” y “concurrentes” son casi antagónicas a “robustas” (por lo menos cuando lo vemos desde el punto de vista de C, C++). Sin embargo, el lenguaje ha tenido éxito logrando su cometido. En el sitio oficial de Erlang podemos ver los diferentes usos que se le ha dado, desde controladores de switch ATM hasta herramientas de testing de software, pasando por un montón de servicios de telefonía. Esas aplicaciones requieren dar servicio ininterrumpido a una cantidad no previsible de usuarios, por lo que deben ser “a prueba de todo”.

Y hablando de Erlang. Otra ventaja que para la programación imperativa parece ciencia ficción, es el cambio de código ejecutable “en caliente”. O sea, mientras la aplicación se está ejecutando. Claro, en Java esto también es posible. Pero impracticable en una aplicación suficientemente grande. Tendrían que serializar todos los objetos que podrían ser afectados. Recargar las clases nuevas, cargar las clases con el código de migración de datos, y esperar a que no haya problemas al cargar todos los objetos serializados a sus nuevas “formas”. Además tendróan que programar el código de migración todas las veces. En Erlang esto es muchísimo mas simple. Una vez que está compilado, se carga . Punto.

Otra propiedad de los programas funcionales es que al ser funciones pueden ser razonados matemáticamente. Es decir, se puede saber si dos funciones son equivalentes, y elegir la más adecuada. Por lo que pueden ser analizados y optimizados fácilmente por un compilador. Esto es lo que vienen haciendo los optimizadores de consultas de las bases de datos hace años. Por qué no habrían de hacerlo con los demás programas? Más aún, es posible conocer fácilmente la correctitud de un programa. Al ser funciones, podemos hacer unidades de prueba, en donde ingresando parámetros conocidos, podamos saber inmediatamente si la función es correcta o no. Y como no hay efectos de lado, estaremos seguros de ello. Por lo que es una característica invaluable para el desarrollo de sistemas robustos.

Resumiendo
En los lenguajes imperativos, los programas ejecutan comandos secuancialmente, usan variables para organizar la memoria, y actualizan las variables con sentencias de asignación.El resultado de los programas por lo tanto es el estado de las variables permanentes (como los archivos) al final de la ejecución. Sin embargo en este modelo no es posible ejecutar segmentos del programa simultáneamente porque in comando puede depender de los cambios realizados a las variables por comandos anteriores. Por lo que la velocidad de ejecución está dada por la velocidad a la que se ejecutan las instrucciones individuales. Además para saber el estado de un cómputo es necesario conocer el estado de todas las variables.
En contraste, los lenguajes de programación funcional no tienen variables, sentencias de asignación ni construcciones iterativas. Su diseño está basado en el concepto de funciones matemáticas. El programa entero es una función, compuesta por otras funciones posiblemente recursivas.
Aunque no hay variables, existen identificadores asociados a valores. Identificadores adquiren valores mediante binding (asociación). Las variables son innecesarias en éste paradigma porque el resultado de una función es inmediatamente pasado como un parámetro a la siguiente. Como no existen variables, tampoco hay efectos de lado. Las funciones son objetos de primera clase.

Enlaces a sitios útiles o al menos informativos :)

About these ads

1 Comentario

Archivado bajo Comentario, Computadoras, Programacion

Una respuesta a “Mi pequeña obsesión con la programación funcional (parte 02)

  1. Aquí hay una traducción del artículo “Functional programming for the rest of us” al español, del que habla esta entrada del blog.

    http://ademirar.wordpress.com/2010/08/28/programacion-funcional-para-el-resto-de-nosotros/

    Saludos.

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s