Haskell in Spanish 1
I've seen very few tutorials or introductions to Haskell written in Spanish, so I decided to write some text in my native language about this great programming language, even though I consider myself still a newbie in Haskell, and therefore the content will be very basic. I also hope to learn more on the way, thanks to the corrections that make others about what I write, and by the fact that I will have to further study in order to write properly on some issues. Now in Spanish.
He visto pocos tutoriales o introducciones sobre Haskell escritos en español, así que he decidido escribir algunas entradas en mi lengua nativa sobre este genial lenguaje de programación, a pesar de que me considero aún un novato en Haskell, y por tanto el contenido en principio será muy básico. Con esto espero también aprender más por el camino, gracias a las correciones que puedan hacer otros sobre lo que escribo, y al hecho de que tendré que estudiar más a fondo para poder escribir con propiedad sobre algunos temas.
Voy a comenzar con una lista de cosas que me han sorprendido de Haskell cuando empecé a interesarme por él. Puesto que estoy ilusionado con lo que he visto, puede que mi estilo en esta primera entrada sea demasiado optimista y positivo, sin ser capaz de indicar las debilidades de Haskell, y únicamente ser capaz de resaltar sus fortalezas y ventajas. Pero por otro lado no hay nada mejor que un poco de pasión para convencer a alguien a adentrarse en un tema nuevo e interesante. Espero que disculpen mis imprecisiones debido a mi euforia a la hora de hablar de estos hechos.
Hechos interesantes sobre Haskell
La primera vez que vi código en Haskell fue en los primeros puestos de la "Google Code Jam". ¡¡Los que ganaban el concurso escribían sus programas en Haskell!! Recuerdo que no entendía nada del código, y que me sorprendió que los programas eran muy cortos, sobre todo comparados con mis soluciones en Python. Me daba la impresión como que no estaban terminados. Pero estaban perfectamente cerrados y solucionaban bien los retos de programación del concurso en el tiempo establecido.
En Haskell no importa el orden de declaración de las funciones: todas se ven con todas (puedes poner la función "main" donde quieras).
El número de palabras reservadas en Haskell es aprox. 2/3 del número de palabras reservadas en C (unas 22 frente a unas 33). Con esto quiero decir que tiene un núcleo muy simple y reducido.
En Haskell no hay palabras reservadas para hacer bucles de ningún tipo (ni for, ni while, ni siquiera algo como goto para emularlo). El truco está en utilizar recursividad y el pattern-matching para conseguir iterar sobre algo muchas veces (hasta que se cumpla alguna condición). Cuando sabes más del lenguaje te das cuenta que el 95% de las veces puedes usar funciones genéricas en vez de bucles (ej: map, fold). Es curioso que ahora es esto lo que se aconseja en las versiones más modernas de lenguajes como C++: siempre que se pueda, utilizar funciones de alto nivel para las iteraciones en vez de bucles explícitos.
Todas las "variables" (o definiciones, o expresiones) en Haskell son inmutables, lo que significa que todas son constantes y no se pueden modificar. Para un programador imperativo esto parece una limitación gigantesca... hasta que te das cuenta de que es una ventaja. Como curiosidad, si escribes
x = x + 1
no estas incrementando x en uno más del número que tenía (si "x" está definido, es inmutable, es decir, constante, y ya no se puede modificar), sino que normalmente estás escribiendo una definición recursiva:x = ((..x.. + 1) + 1) + 1) + 1
(básicamente esta definición es un bucle infinito). No hay que complicarse: si quieres almacenar el siguiente valor de "x" sólo hay que escribir "y = x + 1" ¿no?.Aunque Haskell tiene tipado estático, en la práctica no es necesario declarar ningún tipo, y el lenguaje encuentra automáticamente los mejores tipos para cualquier expresión, pues internamente implementa el más potente sistema de inferencia de tipos que existe (algoritmo de Hindley-Milner). Esto te da la facilidad de uso del tipado dinámico con las ventajas del tipado estático. Otros lenguajes tienen algo parecido, o empiezan a tenerlo (ej: "auto" en C++), pero no suele estar tan integrado en el lenguaje.
Haskell permite escribir código siguiendo "reglas de identado" similares a las de Python (alineando en la misma columna expresiones dentro del mismo bloque), pero también tiene un modo "layout" o de "escritura libre" sin reglas de identado, que utiliza llaves y puntos y coma (a lo C) para escribir expresiones, que permitiría escribir cualquier programa entero en una única línea. Ambos sistemas están activos simultáneamente, y se pueden mezclar (siempre que se haga entre bloques de código distintos).
Haskell acepta también el formato llamado "literate haskell", que consiste en escribir en el fichero del código cualquier texto libre (normalmente formateado como Markdown o ReStructuredText, pero puede ser cualquier cosa) y sólo se considera como texto del programa el código que sigue al carácter '>', cuando dicho carácter es el primero que aparece en una línea (se admiten espacios antes y después). Unido a que las funciones se pueden declarar en cualquier orden y siempre tienen visibilidad en todo el fichero, esto permite escribir una documentación textual muy elaborada, e intercalar el código Haskell en el fichero de texto con dicha documentación. Hay muchas entradas de Blog en Internet escritas en "literate haskell", como ficheros con la extensión "*.lhs", que el compilador acepta exactamente igual que los ficheros que únicamente contienen código Haskell (extensión "*.hs").
Se dice de los programas de Haskell que "si compilan, entonces funcionan", remarcando el hecho de que es uno de los lenguajes de programación que es capaz de encontrar el mayor número de errores de un programa en tiempo de compilación (muchos de los errores que otros lenguajes sólo encuentran en tiempo de ejecución). El tipado estático y estricto, junto con el potente sistema de tipos paramétricos y polimórficos, sobre el que se tiene que basar todo el código que se escribe, es lo que hace esto posible. En muchos casos es capaz de proveer garantías estáticas (es decir, demostradas de manera teórica, y no sólo con una batería de tests) para partes del programa que normalmente suelen ser fuente de errores y difíciles de testar, como las que pueden provocar "condiciones de carrera".
Se dice de Haskell que es "el mejor lenguaje imperativo". No es una broma, pues gracias a la sintaxis de la palabra reservada "do", el código escrito dentro de la Monad IO tiene aspecto totalmente imperativo, pero al no serlo realmente, tiene toda la fiabilidad, seguridad y ventajas del código escrito directamente en el paradigma funcional. Aunque se podrían escribir programas enteros dentro de la Monad IO y trabajar como si Haskell fuera un lenguaje de programación imperativo, la idea es precisamente la contraria: escribir el mínimo código posible dentro de IO y sacar lo máximo posible hacia funciones "puras", con todas las ventajas directas del código funcional puro que ello conlleva. Sólo las funciones con "efectos laterales" ineludibles deben estar dentro de la Monad IO.
El hecho de que Haskell sea funcional "puro" (sin efectos laterales) a la vez que no-estricto (implementado como "lazy" o perezoso) por defecto, lo hace ideal para crear programas concurrentes, que aprovechen al máximo los múltiples procesadores de las arquitecturas actuales. Esto ocurre además de manera natural, pues las funciones puras son thread-safe siempre, y el funcionamiento no-estricto (lazy) permite una planificación implicita de operaciones en paralelo. Esto quizás sea unas de las características más potentes de Haskell, y probablemente el hecho de que en los últimos tiempos tenga más visibilidad e importancia como lenguaje de programación.
La librería STM de Haskell (Software Transactional Memory) se considera la más potente que existe en este campo, aprovechando al máximo el paralelismo y la concurrencia de los procesadores actuales, utilizando un paradigma de programación concurrente muy fácil e intuitivo, sin tener que escribir código difícil de razonar, como el que hay que escribir dentro del infierno de los threads y los mutex, con sus problemas de deadlocks, livelocks, inversión de prioridad, etc... asociados (y sin tener que recurrir de manera explícita a código lock-free aún más complejo y difícil, y aunque en su implementación interna la STM lo utilice, nunca lo expone al usuario de la librería). Por su facilidad, prestaciones y la seguridad que aporta para este tipo de código, la STM se considera el paradigma del futuro para escribir código concurrente. Aunque se puede usar STM en otros lenguajes (hay librerías STM para muchos) es en Haskell donde se integra de manera natural con el lenguaje y donde mejor brilla su potencial y facilidad de uso. La última hornada de procesadores empiezan a implementar Memoria Transaccional por Hardware, mediante extensiones en el juego de instrucciones, lo que portado y aprovechado por esta librería la haría realmente potente, y sin competencia en la creación de programas concurrentes fáciles de escribir y realmente eficientes.
Incluso en la entrada de la Wikipedia sobre la STM (al menos en la inglesa), al final del apartado sobre "ventajas y desventajas conceptuales", se habla de que Haskell está mejor preparado que otros lenguajes al resolver los problemas de la STM en tiempo de compilación: http://goo.gl/cgOrDT, y el apartado siguiente ("Composable operations") es prácticamente exclusivo sobre Haskell.
Como negativo, se dice que la curva de aprendizaje de Haskell es larga y empinada, sobre todo para programadores que provienen del paradigma imperativo y apenas han trabajado con el paradigma funcional. También se dice que si no te rindes y superas esa barrera de entrada, las ventajas son evidentes luego. Por ejemplo, se dice que un programador imperativo tarda en dominar Haskell de manera bastante completa en torno a 1 año de media, siempre que se proponga en serio aprender a manejarlo y no se rinda.
No sólo de Haskell, sino del paradigma de programación funcional en general, se dice que cuando lo dominas, tus programas imperativos son mejores, y tienes más herramientas y abstracciones con las que trabajar a la hora de escribir código de cualquier tipo, incluso aunque sea en un lenguaje que no es principalmente funcional.
Haskell es compilado a código nativo, con lo que es todo lo eficiente en ejecución que puede ser un programa con paradigma funcional (creo que nadie me corregiría si dijera que es el lenguaje funcional que genera el código más eficiente). Pero también tiene un intérprete muy similar al intérprete de Python, que te permite escribir pruebas muy fácilmente, e incluso scripts. Aunque el código interpretado es mucho menos eficiente, esta posibilidad es muy cómoda durante el desarrollo.
Haskell utiliza un recolector de basura: como Java, como C#, como Javascript y como Python. También como lenguajes aún más modernos: como D y como Go. El recolector de basura está integrado en el runtime de Haskell, y por tanto acompaña al código compilado en formato nativo. Esto no podía ser de otra forma: es un lenguaje funcional muy potente, no hay punteros ni elementos de bajo nivel similares, su ejecución no-estricta se implementa como lazy (perezosa) la mayor parte del tiempo, planifica la ejecución concurrente y paralela desde dentro, y todo eso no podría hacerse sin un potente recolector de basura. Podemos pensar que esto creará ejecutables menos eficientes, y puede que sea así, pero debe ser uno de los recolectores de basura más eficientes que existen, cuando muchos de sus ejecutables compiten en eficiencia con ejecutables de código escrito en C. Si además estamos en un entorno multiprocesador y se aprovechan las características de concurrencia de Haskell, el recolector de basura es necesario, y convierte al código en más eficiente, pues se aprovecha su infraestructura para hacer cosas en paralelo y de bajo nivel de manera implícita, que hace que el código escrito apenas tenga que tener en cuenta la concurrencia porque el recolector de basura, y el runtime de Haskell en general, ya se encarga de esas cosas de manera automática internamente. Por cierto, se dice también que cualquier programa lo suficientemente complejo implementa internamente algún tipo primitivo de recolector de basura sin darse demasiado cuenta de ello, y probablemente no de la forma más eficiente que podría hacerse.
Su principal compilador, el GHC (Glasgow Haskell Compiler, que es libre), está escrito también en Haskell (algo raro, pues la mayoría de los compiladores de casi cualquier lenguaje están escritos en C o C++), y lleva décadas desarrollándose. Por supuesto, hay partes de bajo nivel y enganches con el Sistema Operativo escritos en C. Se considera uno de los compiladores más complejos que existen, y durante los últimos años, tanto Haskell en general como GHC en particular se han obsesionado con generar código eficiente para demostrar que los programas compilados de lenguajes funcionales también pueden ser rápidos. El objetivo en su comunidad es el de "batir" a C (ni siquiera a C++), el rey invicto de los ejecutables super-óptimos (y el rey de los lenguajes imperativos). Aunque no lo consigan, es un listón muy alto para un lenguaje funcional, de tan alto nivel y con gran capacidad de abstracción y flexibilidad. Y en el mundo de la programación concurrente y paralela, sobre máquinas multiprocesador, probablemente ya ha conseguido este objetivo. El compilador GHC, además de compilar a código nativo, también puede compilar a código en C.
Es una broma repetida a lo largo de los años de Haskell el decir en sus círculos que debemos tratar de que Haskell permanezca siendo inútil, y que "hay que evitar su éxito a toda costa", porque no querían que la pureza de Haskell se contaminase al introducirse en el mundo real. De hecho, es también relativamente común que algunos de los artículos de Haskell (sobre todo de los principales programadores y desarrolladores del compilador principal) tengan títulos negativos, del tipo: "Haskell es inútil". Hoy en día se comenta en los foros que esto ya no aleja a Haskell del mundo real, sino todo lo contrario: sus fans leen los artículos porque ya saben que el título es una broma, y los despistados lo leen al principio un poco con recelo para luego descubrir que dentro no se critica al lenguaje sino todo lo contrario. Los detractores también van corriendo a leerlo para tener argumentos contra este "lenguaje tan complicado y distinto", para descubrir muchas veces que el artículo les gusta y hasta los "convierte" a favor de Haskell.
El lenguaje tuvo un punto de inflexión cuando se introdujo como base para sus librerías (sobre todo para la parte no-pura de sus librerías más básicas, como la librería estándar IO) principios de la Teoría de Categorías (supuestamente una de las ramas más complejas de las matemáticas, que trata de desplazar a la Teoría de Conjuntos como teoría de base para todas las matemáticas). Esto llevó a usar el concepto de Monad (o mónada) como uno de los patrones de diseño más potentes del lenguaje, y que prácticamente lo convirtió en un lenguaje nuevo (la base estaba bien y casi cualquier lenguaje funcional puede usar y aprovecharse del concepto de Monad, pero a Haskell 98 además le añadieron una palabra reservada, "do", como "sintaxis sugar" para trabajar con Monads siguiendo un estilo que podríamos llamar de "programación imperativa" y que lo hacía todo increíblemente más fácil).
Se dice que lo primero que un aprendiz de Haskell hace cuando empieza a entender el lenguaje es escribir en un blog un artículo (otro más de cientos o miles que ya hay) o tutorial explicando al mundo lo que son las Monads y cómo funcionan. Normalmente no sirve mucho como tutorial para terceros, sino más bien para que el propio autor y aprendiz de Haskell asiente sus ideas.
Haskell es el segundo lenguaje de la historia que implementó evaluación perezosa generalizada y presente por defecto en todo el lenguaje (el primero fue Miranda, que fue un prototipo de lenguaje creado para una investigación sobre este tema y del que Haskell tomó ideas). Y es el primer lenguaje de la historia en utilizar entrada/salida mediante mónadas (Monadic IO), que es su abstracción más potente.
El hecho de ser "perezoso" o "lazy" hace que en Haskell se puedan definir estructuras de datos "infinitas", como por ejemplo una lista con todos los números del 1 hasta el infinito (o un árbol infinito, o un grafo infinito), y trabajar normalmente con ellas. Haskell sólo evaluará la parte que necesite de la lista (por eso es "perezoso") y si al final de las operaciones el resultado es finito, no tendrá problemas en mostrarlo. Esto es una abstración muy útil para determinado tipo de problemas.
Por lo que yo he visto, su única desventaja con Lisp es que no tiene integrado lo que se denomina "las macros de Lisp" (que no tienen nada que ver con las macros de C). Para ello hay una extensión (ya bastante madura, y perfectamente soportada por GHC, el compilador principal) denominada Template Haskell, que permite hacer lo mismo y más (parseo de texto libre y generación de código Haskell en tiempo de compilación, que luego vuelve a compilarse en una segunda fase), pero hay que reconocer que el manejo es un poco más feo y menos elegante que en Lisp (donde las macros tienen el mismo aspecto que el código normal de Lisp: las S-expression). Pero el formato del código normal de Lisp está a su vez limitado y es menos elegante que en Haskell precisamente para poder soportar las macros utilizando el mismo formato del lenguaje (es decir, para conseguir que datos y programas tengan el mismo formato). Con Template Haskell también puedes tratar un programa como si fueran datos, y se creó para cubrir esa deficiencia original respecto a Lisp.
Hay muchos conceptos de programación que normalmente se asocian o implementan como parte intrínseca de los lenguajes de programación que en Haskell se implementan como simples librerías que únicamente utilizan el "core" del lenguaje, demostrando que estas abstracciones o conceptos no son de tan bajo nivel, y que con un lenguaje con una base adecuada, como Haskell, se pueden implementar utilizando sólo unos elementos básicos. Algunos ejemplos de estos conceptos de los que hablo:
Bucles: Bueno, ya he hablado de esto, pero el caso es que basta con tener una capacidad de recursividad potente en el lenguaje, unido a cosas como el pattern-matching, para no necesitar bucles de ningún tipo como elementos intrínsecos del lenguaje.
Programación Imperativa y secuencial: como ya he dicho, mediante el concepto de Monad, y sobre todo con la Monad IO.
Excepciones: Sí, en Haskell existen excepciones, pero no están implementadas como un componente dentro del lenguaje, sino como una librería, por lo que el utilizarlas o no es decisión del programador. Hay que tener en cuenta que algunas librerías base del lenguaje sí las utilizan.
Orientación a Objetos: Aunque es un paradigma distinto y en la programación funcional no encaja, se pueden utilizar conceptos de este paradigma en Haskell (ej: he llegado a leer que las Comonads y la OOP son básicamente lo mismo). Esto no es nada nuevo, pues ya sabemos que hasta en lenguaje C (que no es orientado a objetos) se puede trabajar con este paradigma.
Funciones con parámetros opcionales o con un número de parámetros variable (variadic functions): En Haskell las funciones tienen un número de parámetros fijo (bueno, en realidad tienen un único parámetro y devuelven otra función que coje el siguiente parámetro, y así hasta el último), pero gracias al polimorfismo y las typeclass es posible implementar funciones que "parecen" que tienen un número de parámetros variable. Un ejemplo claro está en la librería "Printf", que implementa una función "printf" al estilo de C (y por tanto con un número de parámetros variable). Es curioso y esclarecedor estudiar cómo se consigue esto, pues no se utiliza ninguna extensión del lenguaje, sino que únicamente con las herramientas que ya tenemos (clases de tipos y tipos paramétricos y polimórficos) es posible que una función con un parámetro de cualquier tipo devuelva a su vez otra función que de nuevo acepta un parámetro de cualquier tipo, y así llegar hasta la función principal que las ejecuta a todas en cadena emulando así la ejecución de una única función multiparámetro. El concepto se asemeja a clases de C++ con funciones que devuelven al propio objeto, de manera que pueden anidarse llamadas a funciones miembro todas las veces que queramos (también muy utilizado en Javascript, en librerías como jQuery). Solo que en Haskell el aspecto externo final es exactamente el mismo que el de una llamada a función con tantos parámetros como hagan falta.
Software importante escrito en Haskell
ghc: El Glasgow Haskell Compiler es el compilador de Haskell más desarrollado, y probablemente el proyecto de software más grande escrito en Haskell.
pandoc: Es una librería para convertir de un formato de marcas a otro, y una utilidad en línea de comandos que utiliza esta librería. En sistemas con LaTeX instalado puede generar salida en PDF.
Puede leer:
Markdown y (subconjuntos de) Textile, reStructuredText, HTML, LaTeX, MediaWiki y DocBook XML.Puede escribir:
Texto plano, Markdown, reStructuredText, XHTML, HTML5, LaTeX (incluidas presentaciones beamer), ConTeXt, RTF, DocBook XML, OpenDocument XML, ODT, Word docx, GNU Texinfo, MediaWiki, EPUB (v2 o v3), FictionBook2, Textile, páginas man, Emacs Org-Mode, AsciiDoc, Slidy, Slideous, DZSlides y S5 HTML.
darcs: Es un sistema de control de versiones distribuido. Como ventaja tiene que está basado en la "teoría de parches" lo que le permite hacer más cosas de manera automática y correcta que otros sistemas como Mercurial o Git (su interfaz por tanto es más sencilla, con menos comandos necesarios). En concreto su sistema de fusionado es el más potente, pero también es su punto débil, pues como desventaja está el rendimiento, ya que su completo sistema de fusionado puede llegar a requerir tiempos exponenciales: mejorar esto es el principal desarrollo que se hace actualmente en este software. Hay que decir que el problema de rendimiento es por diseño, y no por elegir a Haskell. De hecho las primeras versiones de Darcs se escribieron en C++ y tenían el mismo problema.
xmonad: Es un completo gestor de ventanas de tipo mosaico para el sistema de ventanas "X Window System", escrito en menos de 1200 líneas de código. Sus ventajas son que ordena y dimensiona las ventanas automáticamente según varias reglas y criterios, que se pueden configurar e incluso "programar" por el usuario. La idea es que no haga falta el ratón para la gestión de las ventanas. También se considera muy estable, lo que viene por estar escrito en Haskell (dicen que te garantizan que es "crash-free"). Soporta los últimos estándares relacionados con el escritorio en Linux (incluyendo xinerama real, espacios de trabajo distintos para cada pantalla, bandeja del sistema) y es fácilmente extensible. Por ejemplo, sigue los estándares de manera que puede usarse como gestor de ventanas de un escritorio completo como es KDE, sustituyendo a KWin de manera compatible.
snap: Una completa infraestructura de desarrollo web que incluye un servidor web, librerías de apoyo y plantillas para HTML.
yesod: La infraestructura de desarrollo web más avanzada de Haskell. Se la podría comparar con Django o Rails. El servidor web que incluye (warp) es tan rápido como nginx (y a veces incluso más rápido en algunos benchmarks). Hace un uso intensivo de Template Haskell, lo que le permite validar en tiempo de compilación hasta el HTML, CSS y Javascript de las plantillas. También incluye una librería genérica de acceso a bases de datos, que se puede utilizar de manera aislada, sin necesidad de tener todo Yesod instalado.
dfsbuild: Herramienta oficial de Debian para generar los discos de rescate llamados "Debian From Scratch".
pugs: Una implementación completa del lenguaje Perl 6 en Haskell. Con esto se demuestra que se puede utilizar para implementar otros lenguajes.