3 de octubre de 2007

Variables en Haskell

Hoy nos olvidamos un poco de comentar código mal hecho y vamos a hablar del mejor lenguaje del mundo. Haskell.

Una de las caracteristicas que más se comentan de Haskell es que no usa variables destructivas, es decir, que una vez asignado el valor a una variable este permanece inmutable. Esto hace difícil programar código con datos que cambien de estado durante la ejecución del programa. Vamos a ver una manera de mantener y cambiar el estado de una variable. Para empezar, aqui teneis el código que usare. No voy a comentar el código, solo el uso que le voy a dar para crear una variable mutable. Comentare un poco por encima porque funciona al final.

import Data.IORef

incrementa::(Num a)=>IORef a->a->IO a
incrementa v n = do
val <- readIORef v
writeIORef v (val + n)
return (val + n)

creaContador::(Monad m, Num a)=>IORef a->a->m (IO a)
creaContador v n = return (incrementa v n)


Vamos a cargar el código anterior en una sesion interactiva de Haskell (hugs, ghci, ...etc). Lo primero que hacemos es crear un contenedor con el valor cero (fijaos como esquivo llamarlo variable).

ghci> v <- newIORef 0

Luego, usando la funcion creaContador que hemos definido anteriormente, vamos a crear una funcion que mantendrá una cuenta de las veces que se ha llamado. Que facil, ¿no?.

ghci> tick <- creaContenedor v 1

Ahora, si invocamos tick cuatro veces, nos devolvera sucesivamente los valores 1, 2, 3, y 4.

ghci> tick
1
ghci> tick
2
ghci> tick
3
ghci> cuenta <- tick
4
ghci> putStrLn $ "Numero de veces = " ++ (show cuenta)
Numero de veces = 4

Lo primero que tenemos que mirar es v, que es una referencia a una variable mutable. Es decir, es un contenedor para un valor. El valor con el que comienza es cero. Lo de hablar de un contenedor en vez de valor es la manera que tiene Haskell de seguir manteniendo inmutable v, v (el contenedor) permanece inmutable, lo que cambia es el contenido. A estos contenedores se les denominan Monads.

La función incrementa devuelve un valor de tipo IO a. IO también es un contenedor, el valor devuelto es un contenedor con contenido de tipo a. Es de tipo IO porque realiza una computación con la variable mutable, calcula un valor nuevo y lo mete de nuevo en el contenedor.

Y llegamos a la última parte, la función tick. No es una función. También es un contenedor (monad) cuyo contenido es una computación. Cada vez que invocamos tick, lo único que estamos haciendo es volviendo a ejecutar la computación que guarda. Por eso se incrementa el valor referenciado.

Y para terminar con Haskell por hoy. Fijaos en la diferencia entre llamar a creaContenedor y llamar a incrementa directamente, en el siguiente código no se guarda en tick la computación, sino el valor resultante de realizar la computación. Otro día hablamos de las funciones para tratar con monads (contenedores) como do, return y <-.

ghci> tick <- incrementa v 1 
1
ghci> tick
1
ghci> tick
1
ghci> tick
1

2 comentarios:

Marc Ordinas i Llopis dijo...

Vale, creo que lo he entendido, pero... Para qué sirve? Mucho esquivar variables para luego volverlas a poner, en raro :)

Luis Cabellos dijo...

El truco es que la nomenclatura de Haskell puede parecer que hacemos una asignacion de un valor a una variable, pero en realidad oculta llamadas a funciónes. De esta forma sigue siendo un lenguaje funcional puro. Cuando se inicia el cuerpo de una funcion con do, le estamos a indicando a Haskell que estamos en 'modo monaidico'

Donde nosotros ponemos:

do
a <- dameValor
print a


Haskell ve lo siguiente:

bind 0 (\a -> print a)

Es decir, del monad(paquete) 0 coje el valor, se lo pasa a la funcion lambda del segundo parametro de bind y esta usa ese valor referenciandolo como el parametro a.

En realidad la nomenclatura de Haskell es más arisca. El 0 necesita empaquetarlo en un monad con la funcion return, y el bind es el operador >>=.

return 0 >>= (\a -> print a)