Es/Guía de Haskell para autoestopistas

From HaskellWiki
Revision as of 18:54, 22 October 2006 by Madhorse (talk | contribs)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Preámbulo: ¡NO CORRER!

Experiencias recientes de algunos compañeros programadores de C++/Java indican que leyeron varios tutoriales sobre Haskell con “velocidad exponencial” (piensa en como las sesiones de TCP/IP aumentan). Al principio son pocas y prudentes, pero cuando ven que las primeras 3-5 páginas no contienen “nada interesante” en términos de código y ejemplos, empiezan a saltarse párrafos, después capítuos, después páginas enteras, tan sólo decelerando - a menudo hasta parar completamente - alrededor de la página 50, encontrándose con el grueso de conceptos como “clases de tipos”, “constructores de tipos”, “IO monádica”, punto en el que normalmente les entra el pánico, piensa en una escusa perfectamente racional para no seguir leyendo, y se olvida alegremente de este triste y escalofriante encuentro con Haskell (ya que los seres humanos tendemos a olvidar las cosas tristas y escalofriantes).

Este texto pretende introducir al lector a los aspectos prácticos de Haskell desde el principio del todo (los planes para el primer capítulo incluyen: I/O, darcs, Parsec, QuickCheck, depurar y perfilar, por mencionar algunos). Se supone que el lector sabe (donde encontrar) por lo menos los primeros pasos de Haskell: como ejecutar “hugs” o “ghci”, ese diseño es 2-dimensional, etc. Aparte de eso, no esperamos avanzar radicalmente, e iremos paso por paso para no perder a los lectores por el camino. Así que NO CORRER, coge la toalla contigo y continua leyendo.

Si te has saltado el párrafo anterior, me gustaría destacar una vez más que Haskell es sensible a la indentación y espaciado, así que presta atención cuando hagas copias o alineación manual de código en el editor de texto con fuentes proporcionales.

Ah, casi me olvido: el autor está muy interesado en CUALQUIER opinión. Escríbele algunas líneas o palabras (mira Adept para la información de contacto) o propón cambios al tutorial mediante darcs (el repositorio está aquí) o directamente a este Wiki.

Capítulo 1: Omnipresente “¡Hola mundo!” y otras formas de hacer IO en Haskell

Cada capítulo será dedicado a una pequeña tarea real que completaremos desde el principio.

Así que aquí está la tarea para este capítulo: para liberar espacio en tu disco duro para todo el código de haskell que vas a escribir en un futuro cercano, vas a guardar algo de la vieja y polvorienta información en CDs y DVDs. Mientras que grabar CDs (o DVDs) es fácil hoy en día, normalmente ocupa algo (o mucho) tiempo decidir como grabar algunos GB de fotos digitales en CD-Rs, cuando los directorios con las imágenes varían desde 10 a 300 Mb de espacio, y no quieres grabar CDs medio llenos (o medio vacíos).

El ejercicio consiste en escribir un programa que nos ayude a poner un conjunto de directorios en la cantidad mínima posible de discos, al mismo tiempo que aprovechamos cada disco lo máximo posible. Llamemos a este programa “cd-fit”.

Oh. Espera. Hagamos el usual programa “hola mundo”, antes de que nos olvidemos, y después sigamos con cosas más interesante:

-- Cogido de 'hola.hs'
-- A partir de ahora, un comentario al principio del trozo de código
-- especificará el archivo que contiene el programa entero del que fue cogido
-- el trozo. Puedes coger el código del repositorio de darcs
-- "http://adept.linux.kiev.ua/repos/hhgtth" introduciendo el comando 
-- "darcs get http://adept.linux.kiev.ua/repos/hhgtth"
module Main where
main = putStrLn "¡Hola mundo!"

Ejecútalo:

$ runhaskell ./hola.hs
¡Hola mundo!

Vale, ya lo hicimos. Sigamos ahora, no hay nada interesante aquí :)

Cualquier desarrollo serio se debe hacer con un sistema de control de versiones, y no haremos una excepción. Usaremos el moderno sistema de control de versiones distribuido “darcs”. “Moderno” significa que está escrito en Haskell, “distribuido” significa que cada copia que funcione es un repositorio en si mismo.

Primero, creemos un directorio vacío para nuestro todo nuestro código, y ejecuta “darcs init” en él, que creará un subdirectorio “_darcs” para guardar todo lo relacionado con el control de versiones dentro de él.

Inicia tu editor favorito y crea un fichero nuevo llamado “cd-fits.hs” en nuestro directorio de trabajo. Ahora pensemos por un momento como funcionará nuestro programa y expresémoslo en pseudocódigo:

main = leer lista de directorio y sus tamaños
       decidir como ajustarlos a los CD-Rs
       imprimir la solución

¿Parece lógio? Pienso que si.

Vamos a simplificar nuestra vida un poco y asumir por ahora que vamos calcular el espacio de los directorios de alguna forma fuera de nuestro programa (por ejemplo, con “du -sb *”) y leer esa información desde stdin. Convirtamos esto a Haskell:

-- Cogido de 'cd-fit-1-1.hs'
module Main where
 
main = do input <- getContents
          putStrLn ("DEBUG: tengo entrada" ++ input)
	  -- calcular solución e imprimirla

No funciona en realidad, pero bastante parecido al Español, ¿no? Paremos por un momento y miremos más de cerca que hemos escrito línea por línea.

Empecemos desde arriba:

-- Cogido de 'cd-fit-1-1.hs'
input <- getContents

Esto es un ejemplo de la sintaxis de Haskell para hacer IO (en este caso, entrada de datos). Esta línea es una instrucción para leer toda la información disponible desde stdin, devolverla como una única cadena, y unirla al símbolo “input”, de forma que podemos procesar esta cadena de la forma que querramos.

¿Cómo lo sabía? ¿Memoricé todas las funciones? ¡Claro que no! Cada función tiene un tipo, que con su nombre, tiende a decir mucho sobre lo que hace la función.

Lanza un entorno interactivo de Haskell y examinemos esta función de cerca:

$ ghci
   ___         ___ _
  / _ \ /\  /\/ __(_)
 / /_\// /_/ / /  | |      GHC Interactive, version 6.4.1, for Haskell 98.
/ /_\\/ __  / /___| |      http://www.haskell.org/ghc/
\____/\/ /_/\____/|_|      Type :? for help.

Loading package base-1.0 ... linking ... done.
Prelude> :type getContents
getContents :: IO String
Prelude> 

Vemos que “getContents” es una función sin argumentos, que devuelve un “IO String”. El prefijo “IO” significa que es una acción de IO. Devolverá un String, cuando se evalue. La acción será evaluada cuando usemos “<-” para enlazar su resultado a un símbolo.

Ten en cuenta que “<-” no es una forma bonita de asignar un valor a una variable. Es una forma de evaluar (ejecutar) acciones de IO, en otras palabras - para hacer alguna operación de I/O y devolver su resultado (si es que tiene).

Podemos escoger no evaluar la acción que obtenemos de “getContents”, y en vez de eso dejarla por ahí y evaluarla más tarde:

let x = getContents
-- 300 líneas de código
input <- x

Como puedes ver, las acciones de IO pueden ser usadas como valores ordinarios. Supón que hemos construído una lista de acciones IO y hemos encontrado una forma de ejecutarlas una por una. Sería una forma de simular programación imperativa con su notación de “orden de ejecución”.

Haskell te permite hacerlo mejor.

La librería estandard del lenguaje (llamada “Prelude”) nos da acceso a muchas funcinoes que devuelven primitivas de acciones de IO muy útiles. Para combinarlas entre ellas para producir acciones más complejas, usamos “do”:

c = do a <- algunaAcción
       b <- algunaOtraAcción
       print (bar b)
       print (foo a)
       putStrLn "hecho"

De esta forma asociamos “c” a una acción con el siguiente “escenario”:

  • evalua la acción “algunaAcción” y asocia su resultado a “a”
  • después, evalua “algunaOtraAcción” y asocia su resultado a “b”
  • después, procesa “b” con la función “bar” e imprime su resultado
  • después, procesa “a” con la función “foo” e imprime su resultado
  • después, imprime la palabra “hecho”

¿Cuando se ejecutará todo esto en realidad? Respuesta: tan rápido como evaluemos “c” usando “<-” (si devuelve un resultado, como hace “getContents”) o usándola como el nombre de una función (si no devuelve un resultado, como hace “print”):

process = do putStrLn "Procesará algo"
             c
             putStrLn "Hecho"

Date cuenta de que hemos cogido unas cuantas funciones (“algunaAcción”, “algunaOtraAcción”, “print”, “putStrLn”) y usando “do” creamos a partir de ellas una nueva función, que enlazamos al símbolo “c”. Ahora podemos usar “c” como una pieza de construcción para construir una función más compleja “proceso”, y podríamos continuar haciendo esto más y más. Finalmente, algunas de las funciones las mencionaremos en el código de la función “main”, función a la cual la última y más importante acción de IO en cualquier programa de Haskell está asociada.

¿Cuándo se ejececutará/evaluará/forzará “main”? Tan rápido como ejecutemos el programa. Lee esto dos veces y trata de comprenderlo:

La ejecución de un programa de Haskell es una evaluación del símbolo “main” a la que hemos asociado una acción de IO. Mediante esta evaluación obtenemos el resultado de la acción.

Los lectores que estén familiriazados con programación en C++ o Java avanzado y ese conjunto de conocimiento arcano llamado “OOP Patrones de Diseño” se darán cuenta de que “construir acciones a partir de acciones” y “evaluar acciones para obtener resultados” es esencialmente un “Patrón de comando” y “Patrón de composición” combinados. Buenas noticias: en Haskell las tienes para todo tu IO, y lo tienes gratis :)


Ejercicio: Fíjate en el siguiente código:

-- Cogido de 'exercise-1-1.hs'
module Main where
c = putStrLn "C!"

combine before after =
  do before
     putStrLn "En el medio"
     after

main = do combine c c
          let b = combine (putStrLn "¡Hola!") (putStrLn "¡Adios!")
          let d = combine (b) (combine c c)
          putStrLn "¡Tanto tiempo!"

¿Te das cuenta de como indentamos las líneas con cuidado para que el código parezca limpio? En realidad, el código de Haskell tiene que estar alineado de esta forma, o sino no compilará. Si usas tabulados para indentar tus códigos fuente, ten en cuenta que los compiladores de Haskell aumen que el tabstop tiene de ancho 8 caracteres.

A menudo las personas se quejan de que es una forma muy difícil de escribir en Haskell porque requiere que alinees el código. En realidad no es verdad. Si alineas tu código, el compilador adivinará cual es el principio y final de los bloques sintácticos. Sin embargo, si no indentas tu código, puedes especificarlos explícitamente en cada una de las expresiones y usar una distribución arbitraria como en este ejemplo:

-- Cogido de 'exercise-1-2.hs'
combine before after = 
 do { before; 
  putStrLn "En el medio"; 
        after; };
 
main = 
  do { combine c c; let { b = combine (putStrLn "¡Hola!") (putStrLn "¡Adios!")};
         let {d = combine (b) (combine c c)}; 
   putStrLn "¡Tanto tiempo!" };

De vuelta al ejercicio - ¿ves como hacemos código desde la nada? Trata de imaginar que hará este código, después ejecútalo y compruébalo por ti mismo.

¿Entiendas por qué “¡Hola!” y “¡Adiós!” no se imprimen?


Examinemos nuestra función “main” más de cerca:

Prelude> :load cd-fit.hs
Compiling Main             ( ./cd-fit.hs, interpreted )
Ok, modules loaded: Main.
*Main> :type main
main :: IO ()
*Main> 

Vemos que “main” es en realidad una acción IO que no devuelve nada cuando la evaluamos. Cuando combinamos acciones con “do”, el tipo del resultado será el tipo de la última acción, y “putStrLn algo” tiene tipo “IO ()”:

*Main> :type putStrLn "¡Hola mundo!"
putStrLn "¡Hola mundo!" :: IO ()
*Main> 

Ah, por cierto: ¿te has dado cuenta que hemos compilado nuestro primer programa de Haskell para examinar “main”? :)

celebrémoslo poniéndolo bajo control de versión: ejecuta “darcs add cd-fit.hs” y “darcs record”, responder “y” a todas las preguntas y dale un comentario al añadido “Esqueleto de cd-fit.hs”

Vamos a probar a ejecutarlo:

$ echo "foo" | runhaskell cd-fit.hs
DEBUG: tengo entrada foo

Ejercicios:

  • Trata de escribir un programa que coja tu nombre de stdin y te felicite (palabras clave: getLine, putStrLn);
  • Trata de escribir un programa que pregunte por tu nombre, lo lea, te felicite, pregunte por tu color favotiro, y lo devuelva (palabras clave: getLine, putStrLn).