martes, 23 de marzo de 2010

Introducción a Clojure II: inmutabilidad.

"Ningun hombre puede cruzar el mismo río dos veces."
- Heraclito
En la primera entrada introductoria hablabamos de uno de los obstaculos o reparos que alguien acostumbrado a la programación imperativa de lenguajes herederos del c tiene al acercarse a Clojure: la sintaxis prefija y llena de parentesis. Sin embargo hay otra dificultad a la hora de acercarse a Clojure: la inmutabilidad de los datos y todo lo que ello supone, que no es sino otra forma radicalmente diferente de entender la programación. En Clojure, por defecto, no se puede cambiar el valor asociado a un "simbolo" y se delimita muy claramente las formas en que se puede mutar el valor asociado a una referencia.
Por ejemplo algo tan comun en java como:

int i=0;
while (i<10) i="">
es imposible hacerlo en un lenguaje funcional tal cual ya que no existen las "variables" como se entienden en un lenguaje imperativo (ni tampoco los bucles en el sentido imperativo pero es otra historia). Las variables en Clojure son mas bien como símbolos ligados a valores constantes o a funciones.
En clojure podemos definir un símbolo e inicializarlo con un valor que ya no puede cambiar, en lo que sería mas cercano a una constante en java.
;clojure
(def i 10)
// java
final int i=10;
No parece muy util un lenguaje en que no se puedan transformar los datos y en un lenguaje puramente funcional la unica manera de realizar esa transformación es precisamente mediante funciones. Pero las funciones no modifican los valores pasados por parámetros ni las variables globales, sino que siempre producen valores nuevos. En Clojure no se transforma, solo se crea y se destruye.
Esto suena a herejía, a derroche de memoria y lentitud. Y realmente si que lo es pero actualmente la memoria es barata y los procesadores rapidos y en los lenguajes funcionales modernas ese "crear" nuevos valores esta optimizado hasta conseguir un rendimiento bastante razonable.
Otra comparación de código (aunque la comparación no es exacta ni mucho menos, es simplemente para hacerse la idea).
(defn add [x y] (+ x y))

Integer add (final Integer x,final Integer y) {
   return new Integer (x.intValue()+y.intValue());
}

;o con una lista que tal vez se vea mejor
(def add3 [col] (cons 3 col))

Collection add3 (final Collection col) {
   ArrayList aux=new ArrayList (col);
   aux.add (new Integer(3));
   return aux;
}
Bueno esta muy bien pero por ahora solo parece una limitación respecto a java. En realidad sí que se pueden mutar valores en Clojure pero el punto de vista es el contrario: en Clojure por defecto todo es inmutable y asi nos ahorramos los "final", en java es al reves.
¿Que se consigue fomentando la inmutabilidad? Pues algo muy valioso: si no mutan el estado de nada exterior a ellas las funciones no tienen efectos laterales (o dicho de otra forma son transparentes referencialmente). El sabio consejo para cualquier lenguaje de limitar el uso de las variables globales aqui se lleva un paso mas adelante: por defecto no se pueden modificar las variables y por tanto no se producen cambios inesperados. Una función siempre devolvera el mismo valor si es llamada con los mismos parámetros independientemente de cuando, en que orden y como se llame y del estado del resto del universo. Si una función falla, falla siempre, y eso que parece una tonteria es el sueño de cualquiera que haya tenido que hacer debug o tests unitarios de una aplicación java o c++. Tampoco parece muy util el no poder producir efectos laterales ya que hay varias acciones que lo necesitan: la entrada y salida de datos, la generacion de numeros aleatorios, etc. En Clojure las funciones pueden producir esos efectos laterales pero de una forma mas delimitada y controlada (aunque sin llegar a la paranoia de Haskell).
Como conclusion podemos decir que se puede programar en Clojure de forma imperativa si se quiere (al fin y al cabo es un lenguaje multiparadigma) pero el lenguaje fomenta y facilita lo contrario.
Otra ventaja es en el campo de la concurrencia. El complejo análisis (y los horribles bugs que pueden aparecer) del comportamiento de una aplicación con multitud de procesos o hilos de ejecución simultaneos simplemente no es necesario. Los hilos no pueden modificar variables que afecten a los otros porque no pueden modificar las variables.
Hay otras ventajas de este modelo mas simple y matematico de enfocar la programación algunas de las cuales esta en este interesante post sobre la programación funcional.

Sin embargo no todos los objetivos de una aplicación en el mundo real se pueden conseguir (o al menos facilmente) solo con funciones. Los programadores trabajamos con modelos del mundo real que sufren (o disfrutan) modificaciones a lo largo de una línea temporal. Con Clojure tambien podemos hacerlo pero de forma controlada y automatizada en lo que respecta a la concurrencia. No hay que definir los "locks" manualmente ni analizar donde hay que sincronizar la ejecución de los hilos o procesos, al igual que en java ya no tenemos que preocuparnos de gestionar la memoria manualmente como en c. Pero todo esto da para todo un futuro post.