Difference between revisions of "Es/Por que Haskell importa"

From HaskellWiki
Jump to navigation Jump to search
(→‎Epílogo: Categoría)
 
(16 intermediate revisions by 4 users not shown)
Line 74: Line 74:
 
Haskell provee una form elegante, concisa y segura de escribir programas. Los programas no se colgarán inesperadamente, ni producirán basura extraña como salida.
 
Haskell provee una form elegante, concisa y segura de escribir programas. Los programas no se colgarán inesperadamente, ni producirán basura extraña como salida.
   
===Elegance===
+
===Elegancia===
Another property of Haskell that is very important to the programmer, even though it doesn't mean as much in terms of stability or performance, is the elegance of Haskell. To put it simply: stuff just works like you'd expect it to.
+
Otra propiedad de Haskell que es muy importante para el programador, aun cuando no signifique mucho en términos de estabilidad o performance, es la elegancia. Para decirlo sencillamente: las cosas funcionan como tu te lo imaginas.
   
To highlight the elegance of Haskell we shall now take a look at a small example. We choose QuickSort because it's a simple algorithm that is actually useful. We will look at two versions - one written in C++, an imperative language, and one written in Haskell.
+
Para destacar la elegancia de Haskell repasaremos un pequeño ejemplo. Elejimos QuickSort porque es un algoritmo simple y realmente útil. Miraremos dos versiones - una escrita en C++, un lenguaje imperativo, y una escrita en Haskell.
Both versions use only the functionality available to the programmer without importing any extra modules (otherwise we could just call "sort" in each language's standard library and be done with it!). Thus, we use the standard sequence primitives of each language (a "list" in Haskell and an "array" in C++). Both versions must also be polymorphic (which is done "automatically" in Haskell, and with templates in C++). Both versions must use the same recursive algorithm.
+
Ambas versiones sólo usan la funcionalidad disponible para el programador sin importar módulos extras (de otra forma podríamos simplemente invocar a la función "sort" de la biblioteca estándar de cada lenguaje y estaríamos hechos!). Entonces, vamos a usar las primitivas estándar de secuencias de cada lenguaje (una "lista" en Haskell y un "array" en C++). Ambas versiones deben ser polimórficas (lo que es hecho automáticamente por Haskell, y con templates en C++). Ambas versiones deben usar el mismo algoritmo recursivo.
  +
Por favor nota que esto ''no'' tiene la intención de ser una comparación definitiva entre los lenguajes. Sólo se quiere mostrar la elegancia de Haskell, la versión C++ sólo se incluye a los fines de la comparación (y podría haber sido codificada de otra manera si usaramos la Standard Template Library, por ejemplo).
 
Please note that this is ''not'' intended as a definite comparison between the two languages. It's intended to show the elegance of Haskell, the C++ version is only included for comparison (and would be coded quite differently if you used the Standard Template Libraries, for example).
 
   
 
template <typename T>
 
template <typename T>
Line 113: Line 112:
 
};
 
};
   
  +
No explicaremos este código, sólo notemos cúan complejo y difícil es entenderlo en una mirada, principalmente porque el programador tiene que lidiar con detalles de bajo nivel que no tienen nada que ver con la tarea a resolver.
We will not explain this code further, just note how complex and difficult it is to understand at a glance, largely due to the programmer having to deal with low-level details which have nothing to do with the task at hand.
 
  +
Ahora veamos la versión de QuickSort en Haskell, que luce como sigue:
Now, let's take a look at a Haskell version of QuickSort, which might look a something like this.
 
   
 
<haskell>
 
<haskell>
Line 123: Line 122:
 
</haskell>
 
</haskell>
   
  +
Desglosemos este código en detalle, ya que usa mucha sintaxis de Haskell que puedes no conocer.
Let's dissect this code in detail, since it uses quite a lot of Haskell syntax that you might not be familiar with.
 
The function is called qsort and takes a list as a parameter. We define a function in Haskell like so: funcname a b c = expr, where funcname is the name of the function, a, b, and, c are the parameters and expr is the expression to be evaluated (most often using the parameters). Functions are called by simply putting the function name first and then the parameter(s). Haskell doesn't use parenthesis for function application. Functions simply bind more tightly than anything else, so "f 5 * 2", for instance, would apply f to 5 and then multiply by 2, if we wanted the multiplication to occur before the function application then we would use parenthesis like so "f (5*2)".
 
   
  +
La función se llama qsort y toma una lista como su parámetro. Definimos una función en Haskell así: nombrefuncion a b c = expr, donde nombrefuncion es el nombre de la función, a, b, y c son los parámetros y expr es la expresión a ser evaluada (usualmente usando los parámetros). Las funciones se llaman simplemente escribiendo su nombre primero y luego su(s) parámetro(s). Haskell no usa paréntesis para la aplicación funcional. Las funciones simplemente tienen más precedencia que cualquier otra cosa, entonces "f 5 * 2", por ejemplo, aplicaría f a 5 y luego lo multiplicaría por 2; si quisieramos que la multiplicación ocurriera antes de la aplicación entonces usaríamos paréntesis como en "f (5*2)".
Let's get back to QuickSort.
 
First we see that we have two definitions of the functions. This is called pattern matching and we can briefly say that it will test the argument passed to the function top-to-bottom and use the first one that matches.
 
The first definition matches against [] which in Haskell is the empty list (a list of 1,2 and 3 is [1,2,3] so it makes sense that an empty list is just two brackets).
 
So when we try to sort an empty list, the result will be an empty list. Sounds reasonable enough, doesn't it?
 
The second definition pattern matches against a list with at least one element. It does this by using (x:xs) for its argument. The "cons" operator is (:) and it simply puts an element in front of a list, so that 0 : [1,2,3] returns [0,1,2,3]. Pattern matching against (x:xs) is a match against the list with the head x and the tail xs (which may or may not be the empty list). In other words, (x:xs) is a list of at least one element.
 
So since we will need to use the head of the list later, we can actually extract this very elegantly by using pattern matching. You can think of it as naming the contents of the list. This can be done on any data construct, not just a list. It is possible to pattern match against an arbitrary variable name and then use the head function on that to retrieve the head of the list.
 
Now if we have a non empty list, the sorted list is produced by sorting all elements that are smaller than x and putting that in front of x, then we sort all elements larger than x and put those at the end. We do this by using the list concatenation operator ++. Notice that x is not a list so the ++ operator won't work on it alone, which is why we make it a singleton-list by putting it inside brackets.
 
So the function reads "To sort the list, sandwich the head between the sorted list of all elements smaller than the head, and the sorted list of all elements larger than the head". Which could very well be the original algorithm description. This is very common in Haskell. A function definition usually resembles the informal description of the function very closely. This is why I say that Haskell has a smaller semantic gap than other languages.
 
   
  +
Volvamos al QuickSort. primero vemos que tenemos dos definiciones para la función. Esto se llama ajuste de patrones (pattern matching) y brevemente podemos decir que probará el argumento pasado a la función con los patrones desde arriba hacia abajo, usando el primero que se ajuste.
But wait, we're not done yet! How is the list less and more computed? Well, remember that we don't care about sequencing in Haskell, so we've defined them below the function using the where notation (which allows any definitions to use the parameters of the function to which they belong). We use the standard prelude function filter, I won't elaborate too much on this now, but the line less = filter (<x) xs will use filter (<x) xs to filter the list xs. You can see that we actually pass the function which will be used to filter the list to filter, an example of higher order functions.
 
The function (<x) should be read out "the function 'less than x'" and will return True if an element passed to it is less than x (notice how easy it was to construct a function on the fly, we put the expression "<x", "less than x", in parenthesis and sent it off to the function - functions really are just another value!).
 
All elements that pass the test are output from the filter function and put inside less. In a same way (>=x) is used to filter the list for all elements larger than or equal to x.
 
   
  +
La primera definición se ajusta a [] que en Haskell es la lista vacía (una lista de 1, 2 y 3 es [1,2,3] así que tiene sentido que la lista vacía sean sólo dos corchetes).
Now that you've had the syntax explained to you, read the function definition again. Notice how little time it takes to get an understanding about what the function does. The function definitions in Haskell explain what it computes, not how.
 
  +
Así que cuando tratamos de ordenar la lista vacía, el resultado será una lista vacía. ¿Suena razonable, no?
   
  +
El segundo patrón de definición se ajusta a una lista con al menos un elemento. Lo hace usando (x:xs) como su argumento. El operador "cons" es (:) y simplemente pone un elemento en frente de una lista, así 0 : [1,2,3] devuelve [0,1,2,3]. El ajuste de patrones contra (x:xs) se cumple con una lista con cabeza x y cola xs (que puede o no ser la lista vacía). En otras palabras, (x:xs) es una lista de al menos un elemento.
If you've already forgotten the syntax outlined above, don't worry! We'll go through it more thoroughly and at a slower pace in the tutorials. The important thing to get from this code example is that Haskell code is elegant and intuitive.
 
   
  +
Así que como necesitaremos usar la cabeza de la lista después, podemos extraerla muy elegantemente mediante el ajuste de patrones. Puedes pensarlo como nombrar el contenido de la lista. Esto se puede hacer para cualquier construcción de datos, no sólo una lista. Es posible ajustar patrones con un nombre de variable arbitrario y luego usar la función head para obtener la cabeza de la lista.
===Haskell and Bugs===
 
We have several times stated that various features of Haskell help fight the occurrence of bugs. Let's recap these.
 
   
  +
Ahora si tenemos una lista no vacía, la lista ordenada se produce ordenando todos los elementos menores a x y poniéndolos en frente de x, y luego ordenando todos los elementos mayores a x y poniéndolos al final. Hacemos esto usando el operador de concatenación de listas ++. Nótese que x no es una lista así que el operador ++ no funcionará sobre ella, por lo que hacemos una lista unitaria al ponerla entre corchetes.
Haskell programs have less bugs because Haskell is:
 
   
  +
Así, la función se lee "Para ordenar la lista, ubica la cabeza entre la lista ordenada de todos los elementos menores a ella y la lista ordenada de todos los elementos mayores a ella". Esta frase podría bien ser la descripción original del algoritmo. Esto es muy común en Haskell. Una definición de función usualmente se asemeja mucho a la descripción informal de la función. Por esto es que decimos que Haskell tiene una brecha semántica menor que otros lenguajes.
* '''Pure'''. There are no side effects.
 
  +
 
  +
Pero espera, ¡no hemos terminado todavía! ¿Cómo es que las listas less y more se calculan? Bueno, recuerda que no nos importa la secuenciación en Haskell, así que las hemos definido debajo de la función usando notación where (que permite usar los parámetros de la funcion en sus definiciones). Usamos la función del preludio estándar filter. No explicaremos demasiado esta parte, pero la línea less = filter (<x) xs usará filter (<x) xs para filtrar la lista xs. Puedes ver que realmente pasamos la función que se usará para filtrar la lista a filter, un ejemplo de funciones de alto orden.
* '''Strongly typed'''. There can be no dubious use of types. And No Core Dumps!
 
  +
  +
La función (<x) debe leerse "la función 'menor a x'" y devolverá True si el elemento que se le pasa es menor que x (nótese qué fácil fue construir una función al vuelo, ponemos la expresión "<x", "menor a x", entre paréntesis y la mandamos a la función - ¡las funciones en realidad son sólo otro valor!).
  +
  +
Todos los elementos que pasan la prueba son la salida de filter y terminan en less. De la misma manera, (>=x) se usa para filtrar la lista por todos los elementos mayores o iguales a x.
  +
  +
Ahora que te han explicado la sintaxis, lee la definición de la función de nuevo. Nota qué poco tiempo toma lograr entender lo que la función hace. Las definiciones de funciones en Haskell explican lo que computan, no cómo lo hacen.
  +
  +
Si ya te has olvidado de la sintaxis esbozada arriba, ¡no te preocupes! La cubriremos de forma más completa y minuciosa en los tutoriales. Lo importante de este ejemplo es que el código Haskell es elegante e intuitivo.
  +
  +
===Haskell y los errores===
  +
Hemos dicho varias veces que muchas características de Haskell ayudan a disminuir la ocurrencia de errores; recapitulemos esas características.
  +
  +
Los programas en Haskell tienen menos errores porque Haskell es:
  +
  +
* '''Puro'''. No hay efectos laterales.
 
 
  +
* '''Fuertemente tipado'''. No puede haber uso dudoso de tipos. Y no hay core dumps!
* '''Concise'''. Programs are shorter which make it easier to look at a function and "take it all in" at once, convincing yourself that it's correct.
 
 
 
  +
* '''Conciso'''. Los programas más cortos facilitan mirar una función y "captarla" de una sóla vez, convenciendote que es correcta.
* '''High level'''. Haskell programs most often reads out almost exactly like the algorithm description. Which makes it easier to verify that the function does what the algorithm states. By coding at a higher level of abstraction, leaving the details to the compiler, there is less room for bugs to sneak in.
 
 
 
  +
* '''Alto nivel'''. Los programas de Haskell casi siempre se escriben exactamente igual que la descripción del algoritmo. Lo que facilita verificar que la función haga lo que el algoritmo dice. Al codificar en un nivel de abstracción superior, dejando los detalles para el compilador, se deja menos espacios para que se deslicen errores.
* '''Memory managed'''. There's no worrying about dangling pointers, the Garbage Collector takes care of all that. The programmer can worry about implementing the algorithm, not book-keeping of the memory.
 
 
 
  +
* '''Memoria administrada'''. No hay que preocuparse por los punteros, el Garbage Collector se ocupa de ellos. El programador puede preocuparse de la implementación del algoritmo, no de administración de memoria.
* '''Modular'''. Haskell offers stronger and more "glue" to compose your program from already developed modules. Thus Haskell programs can be more modular. Often used modular functions can thus be proven correct by induction. Combining two functions that are proven to be correct, will also give the correct result (assuming the combination is correct).
 
  +
  +
* '''Modular'''. Haskell ofrece más "pegamento" y más fuerte para componer programas a partir de módulos ya desarrollados. De esta manera, los programas Haskell pueden ser más modulares. Frecuentemente se puede probar que el uso de funciones modulares es correcto por inducción. Combinar dos funciones que se probaron correctas dará un resultado correcto (asumiendo que la combinación es correcta).
  +
Aun más, la mayoría de las personas concuerdan en que uno piensa diferente al resolver problemas en un lenguaje funcional. Uno subdivide el problema en funciones más y más pequeñas y luego uno escribe esas funciones pequeñas (y "garantizadas-de-corrección-casi-siempre"), que son compuestas de varias maneras para obtener el resultado final. ¡Simplemente no hay espacio para errores!
   
  +
==Haskell vs POO ==
Furthermore most people agree that you just think differently when solving problems in a functional language. You subdivide the problem into smaller and smaller functions and then you write these small (and "almost-guaranteed-to-be-correct") functions, which are composed in various ways to the final result. There just isn't any room for bugs!
 
  +
El gran beneficio de la programación orientada a objetos (POO) no es que uno pueda agrupar los datos con las funciones que actúan sobre ellos en un objeto, el beneficio es que permite un buen encapsulamiento de datos (separando la interfaz de la implementación) y polimorfismo (permitiendo que un conjunto de tipo de datos se comporte de la misma manera). Sin embargo:
   
  +
''¡Encapsulamiento de datos y polimorfismo no son exclusivos de POO!''
== Haskell vs OOP ==
 
The great benefit of Object Oriented Programming is rarely that you group your data with the functions that act upon it together into an object - it's that it allows for great data encapsulation (separating the interface from implementation) and polymorphism (letting a whole set of data types behave the same way). However:
 
   
  +
Haskell tiene herramientas para abstraer datos. No podemos meternos en ello sin haber visto el sistema de módulos y cómo funcionan los tipos abstractos de datos (TAD) en Haskell, algo que está bastante más allá del alcance de este ensayo. Por ello, haremos una corta descripción de cómo funcionan los TADs y el polimorfismo en Haskell.
''Data encapsulation and polymorphism are not exclusive to OOP!''
 
   
  +
El encapsulamiento en Haskell se logra declarando cada tipo de datos en un módulo independiente, y de este módulo sólo se exporta la interfaz. Internamente puede haber muchas funciones que manipulan los datos reales, pero la interfaz es lo único visible desde fuera del módulo.
Haskell has tools for abstracting data. We can't really get into it without first going through the module system and how abstract data types (ADT) work in Haskell, something which is well beyond the scope of this essay. We will therefore settle for a short description of how ADTs and polymorphism works in Haskell.
 
  +
Notemos que el tipo de datos y las funciones que actúan sobre él no están agrupadas en un "objeto", sino que están (típicamente) agrupadas en el mismo módulo; de esta manera uno puede elegir exportar sólo ciertas funciones (y no los constructores para el tipo de datos), logrando así que estas funciones sean la única forma de manipular el tipo de datos - quedando "escondida" la implementación tras la interfaz.
   
  +
El polimorfismo se logra usando lo que se llama clases de tipo. Si tu tienes conocimientos previos de C++ o Java, puedes asociar las clases con algo parecido a un template para construir un objeto, pero eso no es lo que son las clases en Haskell. Una clase de tipo es realmente eso a lo que suena. Es un conjunto de reglas para determinar si una instancia de un tipo es una instancia de la clase.
Data encapsulation is done in Haskell by declaring each data type in a separate module, from this module you only export the interface. Internally there might be a host of functions that touch the actual data, but the interface is all that's visible from outside of the module.
 
  +
Haskell separa la instanciación de la clase de la construcción del tipo de datos. Uno puede declarar un tipo "Porsche" como una instancia de la clase de tipo "Auto", digamos. Todas las funciones que pueden ser aplicadas a cualquier otro miembro de la clase de tipo Auto podrán ser aplicadas a un Porsche.
Note that the data type and the functions that act upon the data type are not grouped into an "object", but they are (typically) grouped into the same module, so you can choose to only export certain functions (and not the constructors for the data type) thus making these functions the only way to manipulate the data type - "hiding" the implementation from the interface.
 
  +
Una clase que está incluida en Haskell es la clase de tipo Show, y un tipo puede ser instanciado en esa clase dando una función show, que convierta una instancia de ese tipo en un String. Consecuentemente, las instancias de casi todos los tipos de Haskell pueden ser mostradas en la pantalla aplicando la función show para convertirlos en un String, y usando después la acción de E/S correspondiente (hay más sobre E/S en los tutoriales).
   
  +
Notemos cuán similar es esto con la noción de objeto en POO cuando se considera el aspecto del polimorfismo.
Polymorphism is done by using something called type classes. Now, if you come from a C++ or Java background you might associate classes with something resembling a template for how to construct an object, but that's not what they mean in Haskell. A type class in Haskell is really just what it sounds like. It's a set of rules for determining whether a type is an instance of that class.
 
  +
El sistema de Haskell es más intuitivo para manejar el polimorfismo. Uno no necesita preocuparse en heredar en el orden correcto o asegurarse que la herencia tiene sentido. Una clase es simplemente una clase, y los tipos que son instancias de esta clase no tienen que compartir una relación de herencias padre-hijo. Si un tipo de datos cumple con los requisitos de una clase, entonces puede ser instanciada en esa clase. Simple, no?
So Haskell separates the class instantiation and the construction of the data type. You might declare a type "Porsche", to be an instance of the "Car" type class, say. All functions that can be applied onto any other member of the Car type class can then be applied to a Porsche as well.
 
  +
¿Recuerdas el ejemplo de QuickSort? ¿Recuerdas que habíamos dicho que era polimórfico? El secreto detrás del polimorfismo en qsort es que está definida para trabajar sobre listas de cualquier tipo que pertenezca a la clase Ord (por "Ordenado"). Ord tiene definida un conjunto de funciones, entre ellas "<" y ">" que son suficientes para nuestras necesidades, porque sólo necesitamos saber si un elemento es mayor que x o no. Por ello si fueramos a definir las funciones que requiere Ord para nuestro tipo Porsche (sería suficiente implementar <= y ==, Haskell construiría el resto a partir de ellas) en una instanciación de la clase Ord, podríamos usar qsort para ordenar listas de Porsche (aun cuando no tenga sentido ordenar Porsches). Notemos que nunca dijimos nada sobre las clases a las que debían pertenecer los elementos de la lista, Haskell inferirá esto automáticamente viendo las funciones que hemos usado (en el ejemplo de qsort, solo "<" y ">=" son relevantes).
A class that's included with Haskell is the Show type class, for which a type can be instantiated by providing a show function, which converts the data type to a String. Consequently almost all types in Haskell can be printed onto the screen by applying show on them to convert them to a String, and then using the relevant IO action (more on IO in the tutorials).
 
Note how similar this is to the the object notion in OOP when it comes to the polymorphism aspect.
 
The Haskell system is a more intuitive system for handling polymorphism. You won't have to worry about inheriting in the correct hierarchical order or to make sure that the inheritance is even sensible. A class is just a class, and types that are instances of this class really doesn't have to share some parent-child inheritance relationship. If your data type fulfills the requirements of a class, then it can be instantiated in that class. Simple, isn't it?
 
Remember the QuickSort example? Remember that I said it was polymorphic? The secret behind the polymorphism in qsort is that it is defined to work on any type in the Ord type class (for "Ordered"). Ord has a set of functions defined, among them is "<" and ">=" which are sufficient for our needs because we only need to know whether an element is smaller than x or not. So if we were to define the Ord functions for our Porsche type (it's sufficient to implement, say, <= and ==, Haskell will figure out the rest from those) in an instantiation of the Ord type class, we could then use qsort to sort lists of Porsches (even though sorting Porsches might not make sense).
 
Note that we never say anything about which classes the elements of the list must be defined for, Haskell will infer this automatically from just looking at which functions we have used (in the qsort example, only "<" and ">=" are relevant).
 
   
So to summarize: Haskell does include mechanisms for data encapsulation that match or surpass those of OOP languages. The only thing Haskell does not provide is a way to group functions and data together into a single "object" (aside from creating a data type which includes a function - remember, functions are data!). This is, however, a very minor problem. To apply a function to an object you would write "func obj a b c" instead of something like "obj.func a b c".
+
Para resumir: Haskell incluye mecanismos para encapsulamiento de datos que igualan o pasan a aquellos de los lenguajes orientados a objetos. Lo único que Haskell no provee es una forma de agrupar funciones y datos en un objeto (aparte de construir un tipo de datos que incluya una función, recuerda que las funciones son datos!). Sin embargo, esto es un problema menor: para aplicar una función a un objeto, escribiremos "func obj a b c" en vez de algo como "obj.func a b c".
   
  +
== Modularidad ==
  +
Un concepto central en la informática es la modularidad. Una analogía popular es esta: digamos que tu quieres construir una silla de madera. Si tu construyes las partes por separado y luego las pegas, la tarea se resuelve fácilmente. Pero si tu tienes que tallar la silla entera a partir de un pedazo sólido de madera, te puede resultar un poco más difícil. John Hughes tiene algo para decir sobre el tema en este artículo: [http://www.cs.chalmers.se/~rjmh/Papers/whyfp.html Why Functional Programming Matters] (Por qué es importante la programación funcional)
   
  +
''"Los lenguajes que intenten mejorar la productividad deben permitir la programación modular. Pero nuevas reglas de alcance y mecanismos para compilación separada no son suficientes - modularidad significa más que módulos. Nuestra capacidad para descomponer problemas en partes depende directamente de nuestra capacidad para unir las soluciones. Para ayudar a la programación modular, un lenguaje deber proveer buena cola.''
== Modularity ==
 
A central concept in computing is modularity. A popular analogy is this: say you wanted to construct a wooden chair. If you construct the parts of it separately, and then glue them together, the task is solved easily. But if you were to carve the whole thing out of a solid piece of wood, it would prove to be quite a bit harder. John Hughes had this to say on the topic in his paper [http://www.cs.chalmers.se/~rjmh/Papers/whyfp.html Why Functional Programming Matters]
 
   
  +
''Los lenguajes de programación funcional proveen dos nuevos tipos de colas: funciones de alto orden y evaluación perezosa."''
''"Languages which aim to improve productivity must support modular programming well. But new scope rules and mechanisms for separate compilation are not enough - modularity means more than modules. Our ability to decompose a problem into parts depends directly on our ability to glue solutions together. To assist modular programming, a language must provide good glue.''
 
   
  +
==La velocidad de Haskell==
''Functional programming languages provide two new kinds of glue - higher-order functions and lazy evaluation."''
 
  +
Dejemos en claro que lo que sigue se aplica sólo en el caso general en el que la velocidad no es algo crítico en absoluto, donde se pueda aceptar tiempos de ejecución un poco más largos, si se reducen en gran medida los tiempos de desarrollos. Hay casos en los que la velocidad es de importancia primordial, en esos casos no se aplica esta sección en el mismo grado.
   
  +
Algunos programadores de C++ pueden afirmar que la versión de QuickSort en C++ es probablemente un tanto más rápida que la versión en Haskell. Y esto puede ser cierto. Sin embargo, para la mayoría de las aplicaciones la diferencia en velocidad es tan pequeña que es completamente insignificante. Por ejemplo, mira [http://shootout.alioth.debian.org/gp4/benchmark.php?test=all&lang=all Computer Language Shootout], donde Haskell logra resultados favorables respecto a la mayoría de los lenguajes llamados "rápidos". Si bien es cierto, que esas pruebas no prueban nada sobre la performance en el mundo real, sí muestran que Haskell no es tan lento como algunas personas piensan. Al momento de escribir esto está en la 2da posición, sólo levemente detrás de C (con C++ bastante más lejos).
   
  +
Casi todos los programas en uso actualmente tienen una distribución bastante pareja en tiempo de procesamiento entre sus funciones. Las excepciones más notables son aplicaciones como encoders MPEG y conjuntos de pruebas artificiales, donde gran parte del tiempo de ejecución se pasa en pequeñas porciones del código. Si uno ''realmente'' necesita velocidad a cualquier costo, entonces se debe considerar usar C en vez de Haskell.
==The Speed of Haskell ==
 
Let me first state clearly that the following only applies to the general case in which speed isn't absolutely critical, where you can accept a few percent longer execution time for the benefit of reducing development time greatly. There are cases when speed is the primary concern, and then the following section will not apply to the same extent.
 
   
  +
Hay una vieja regla en programación llamada la regla del "80/20". Esta regla dice que el 80% del tiempo se consume en 20% del código. La consecuencia de esto es que una función cualquiera de un sistema tiene una importancia mínima cuando se trata de optimización de velocidad. Puede haber un puñado de funciones suficientemente significativas a la hora de optimizar. Estas funciones podrían ser escritas en C (usando la excelente interfaz para funciones foráneas de Haskell). El rol que tiene ahora la programación en Assembler podría, y probablemente lo sea, ocupado por C - es decir, usándolo para las partes donde el tiempo es realmente crítico, pero no para el sistema entero.
Now, some C++ programmers might claim that the C++ version of QuickSort above is probably a bit faster than the Haskell version. And this might be true. For most applications, though, the difference in speed is so small that it's utterly irrelevant. For instance, take a look at the [http://shootout.alioth.debian.org/gp4/benchmark.php?test=all&lang=all Computer Language Shootout], where Haskell compares favorably to most of the so called "fast" languages.
 
Now, these benchmarks don't prove all that much about real-world performance, but they do show that Haskell isn't as slow as some people think. At the time of writing it's in 2nd position, only slightly behind C (with C++ fairly far behind).
 
   
  +
Deberíamos seguir moviéndonos a niveles de abstracción más altos, como lo hicimos antes. Debemos cambiar la velocidad de las aplicaciones por mejores productividad, estabilidad y mantenibilidad. El tiempo de los programadores es casi siempre más caro que el tiempo de CPU.
Almost all programs in use today have a fairly even spread of processing time among its functions. The most notable exceptions are applications like MPEG encoders, and artificial benchmarks, which spend a large portion of their execution time within a small portion of the code. If you ''really'' need speed at all costs, consider using C instead of Haskell.
 
  +
No escribimos más aplicaciones en Assembler por la misma razón por la que no deberíamos escribir más aplicaciones en C.
   
  +
Finalmente recordemos que optimizar los algoritmos puede traer resultados mucho mejores que optimizar el código. Para ejemplos teóricos donde ciertos factores como el tiempo de desarrollo y la estabilidad no cuentan, entonces seguro que C es más rápido que Haskell. Pero en el mundo real, donde los tiempos de desarrollo ''sí'' importan, no es el caso. Si se puede desarrollar una aplicación en Haskell en la décima parte de lo que tomaría desarrollarla en C (la experiencia dice que esto no es tan poco común), entonces uno tendría mucho más tiempo para analizar e implementar nuevos algoritmos. Por ello, en el "mundo real" donde no tenemos infinita cantidad de tiempo para programar nuestras aplicaciones, los programas en Haskell pueden ser a menudo mucho más rápidos que los programas en C.
There's an old rule in computer programming called the "80/20 rule". It states that 80% of the time is spent in 20% of the code. The consequence of this is that any given function in your system will likely be of minimal importance when it comes to optimizations for speed. There may be only a handful of functions important enough to optimize. These important functions could be written in C (using the excellent foreign function interface in Haskell). The role of C could, and probably will, take over the role of assembler programming - you use it for the really time-critical bits of your system, but not for the whole system itself.
 
   
  +
== Epílogo ==
We should continue to move to higher levels of abstraction, just like we've done before. We should trade application speed for increased productivity, stability and maintainability. Programmer time is almost always more expensive than CPU time.
 
We aren't writing applications in assembler anymore for the same reason we shouldn't be writing applications in C anymore.
 
   
  +
¿Por qué no es tan popular el Haskell como otros lenguajes de programación? Si el sistema operativo está escrito en C (o en algún otro lenguaje de programación imperativo) es muy probable que sea más fácil utilizar un lenguaje imperativo para interactuar con él. Otra posible razón es que los lenguajes de programación muy raramente se consideran herramientas intercambiables (a pesar de que lo son). Para la mayoría de la gente su lenguaje de programación preferido es similar a una religión; es difícil creer que puede existir otro lenguaje mejor y más rápido. Paul Graham escribió un documento llamado "[http://www.paulgraham.com/avg.html Beating the Averages]" ("Ganándole a los promedios") en el cual cuenta sus experiencias utilizando Lisp (otro lenguaje de programación funcional) en una empresa nueva. Graham utiliza una analogía a la que llama "La paradoja de Blub". Es algo así: suponiendo que el lenguaje de programación preferido de una persona sea "Blub" ("Blub" siendo un lenguaje de programación ficticio de poder medio), esta persona generalmente sólo podrá identificar lenguajes de programación de menor poder que "Blub". El programador entonces examina COBOL y piensa "¿Como puede alguien programar en COBOL, si no soporta X?" (tomando X como un característica que está presente en "Blub"). Sin embargo, esta persona tiene dificultades para juzgar lenguajes que están por encima de "Blub" en la escala. Si examina estos lenguajes, le parecerán "raros" porque el programador está "pensando en Blub" y no tiene posibilidad de comprender características avanzadas de lenguajes de programación más poderosos. Esto invariablemente lleva a la conclusión que para poder comparar todos los lenguajes uno debe estar en la cima de la escala de "poder". Yo creo que los lenguajes funcionales casi por definición están más cerca de la cima que los imperativos, así es que los lenguajes pueden limitar el campo de pensamiento de un programador. Si solamente has programado "Blub" puede que no veas sus limitaciones; se harán evidentes cambiando a un lenguaje más poderoso.
Finally remember that algorithmic optimization can give much better results than code optimization. For theoretical examples when factors such as development times and stability doesn't matter, then sure C is often faster than Haskell. But in the real world development times ''do'' matter, this isn't the case. If you can develop your Haskell application in one tenth the time it would take to develop it in C (from experience, this is not at all uncommon) you will have lots of time to profile and implement new algorithims. So in the "real world" where we don't have infinite amounts of time to program our applications, Haskell programs can often be much faster than C programs.
 
   
  +
Haskell no se utiliza más a menudo porque la gente siente que "su" lenguaje hace "todo lo necesario"; y no se equivocan, ya que están pensando en "Blub". Pero los lenguajes de programación no son solamente tecnología, también son una forma de pensar; es muy difícil comprender la utilidad de Haskell si no se piensa en Haskell.
   
  +
Con un poco de suerte, este artículo puede haberte ayudado a salir de la paradoja del "Blub". Aunque quizás todavía no "Pienses en Haskell", espero que al menos comiences a tomar conciencia de las limitaciones que impone en el pensamiento tu lenguaje de Introduprogramación "favorito" y que a partir de ahora tengas mayor motivación para expandir tus ideas. Si estás decidido a aprender un lenguaje funcional de programación para poder tener una vista mejor de la "escala de poder", entonces el Haskell es un candidato excelente.
== Epilogue ==
 
So if Haskell is so great, how come it isn't "mainstream"? Well, one reason is that the operating system is probably written in C or some other imperative language, so if your application mainly interacts with the internals of the OS, you may have an easier time using imperative languages. Another reason for the lack of Haskell, and other functional languages, in mainstream use is that programming languages are rarely thought of as tools (even though they are). To most people their favorite programming language is much more like religion - it just seems unlikely that any other language exists that can get the job done better and faster.
 
There is a paper by Paul Graham called [http://www.paulgraham.com/avg.html Beating the Averages] describing his experience using Lisp, another functional language, for an upstart company. In it he uses an analogy which he calls "The Blub Paradox".
 
It goes a little something like this:
 
If a programmer's favorite language is Blub, which is positioned somewhere in the middle of the "power spectrum", he can most often only identify languages that are lower down in the spectrum. He can look at COBOL and say "How can anyone get anything done in that language, it doesn't have feature x", x being a feature in Blub.
 
However, this Blub programmer has a harder time looking the other way in the spectrum. If he examines languages that are higher up in the power spectrum, they will just seem "weird" because the Blub programmer is "thinking in Blub" and can not possibly see the uses for various features of more powerful languages. It goes without saying that this inductively leads to the conclusion that to be able to compare all languages you'll need to position yourself at the top of the power spectrum. It is my belief that functional languages, almost by definition, are closer to the top of the power spectrum than imperative ones.
 
So languages can actually limit a programmers frame of thought. If all you've ever programmed is Blub, you may not see the limitations of Blub - you may only do that by switching to another level which is more powerful.
 
   
One of the reasons the mainstream doesn't use Haskell is because people feel that "their" language does "everything they need". And of course it does, because they are thinking in Blub! Languages aren't just technology, it's a way of thinking. And if you're not thinking in Haskell, it is very hard to see the use of Haskell - even if Haskell would allow you to write better applications in a shorter amount of time!
 
   
  +
{{traduccion|titulo=Why Haskell matters}}
Hopefully this article has helped you break out of the Blub paradox. Even though you may not yet "think in Haskell", it is my hope that you are at least aware of any limitations in your frame of thought imposed by your current "favorite" language, and that you now have more motivation to expand it by learning something new.
 
If you are committed to learn a functional language, to get a better view of the power spectrum, then Haskell is an excellent candidate.
 
   
[[Category:Tutoriales]]
+
[[Category:Es/Tutoriales|Por qué Haskell importa]]

Latest revision as of 19:33, 10 November 2006

Por qué Haskell importa

¿Qué son los lenguajes de programación funcionales?

Los lenguajes de programación como C/C++/Java/Python se llaman imperativos porque consisten en una secuencia de acciones. El programador explícitamente le dice a la computadora cómo realizar una tarea, paso a paso. Los lenguajes de programación funcionales trabajan de forma diferente. En lugar de realizar acciones en secuencia, evalúan expresiones.

El nivel de Abstracción

Hay dos áreas que son fundamentales al programar una computadora - manejo de recursos y secuenciación. El manejo de recursos (alojar registros y memoria) ha sido objeto de mucha abstracción: la mayoría de los lenguajes nuevos (imperativos y funcionales) han implementado recolección de basura para eliminar el manejo de recursos del problema, dejando al programador concentrarse en el algoritmo en lugar de la tarea mecánica del alojamiento de memoria.

La secuenciación también tuvo algo de abstracción, aunque no al mismo nivel. Los lenguajes imperativos lo han hecho introduciendo nuevas palabras claves y librerías estándar. Por ejemplo, la mayoría de los lenguajes imperativos tienen sintaxis especial para construir varios bucles ligeramente diferentes, ya no tienes que hacer todas las tareas de manejar estos bucles tú mismo.

Pero los lenguajes imperativos están basados en la noción de secuenciación - no pueden escapar de ella completamente. La única forma de elevar el nivel de abstracción en el área de secuenciación para un lenguaje imperativo es introduciendo más palabras clave o funciones estándar, abarrotando el lenguaje.

Esta relación cercana entre lenguajes imperativos y la tarea de secuenciar comandos para que el procesador ejecute significa que los lenguajes imperativos nunca podŕan elevarse más allá de la tarea de secuenciar, y como tales nunca pueden llegar al mismo nivel de abstracción que los lenguajes funcionales.

En Haskell, la secuenciación es eliminada. Sólo se preocupa por lo que el programa calcula, no cuándo o cómo se calcula. Esto hace de Haskell un lenguaje más flexible y fácil de usar. Haskell tiende a ser parte de la solución a un problema, y no parte del problema en sí.

Funciones y Efectos Laterales en Lenguajes Funcionales

Las funciones tienen un rol importante en los lenguajes de programación funcional. Las funciones se consideran valores, como Int o String. Una función puede devolver otra función, puede tomar otra función como parámetro y puede ser construida componiendo funciones. Esto ofrece una "cola" más fuerte para combinar los módulos de tu programa. Una función que evalúa alguna expresión puede formar parte de la computación como un argumento, por ejemplo; haciendo así más modular a la función. También puedes tener que una función contruye otra función. Por ejemplo, se puede definir una función "diferenciar" que diferenciará una función numéricamente dada. Entonces, si tienes una función "f", puedes definir "f' = differentiate f", y usarla como lo harías en un contexto matemático. Estos tipos de funciones se llaman funciones de orden superior.

Aquí hay un pequeño ejemplo en Haskell de una función numOf que cuenta el número de elementos en una lista que satisface cierta propiedad.

numOf p xs = length (filter p xs)

Después discutiremos la sintaxis de Haskell, pero lo que esta línea dice es "Para obtener el resultado, filtra la lista xs con la prueba p y computa el largo del resultado". Ahora p es una función que toma un elemento y devuelve True (verdadero) o False (falso) determinando si el elemento pasa o no la prueba. numOf es una función de orden superios, parte de su funcionalidad es tomada como un argumento. Se debe notar que filter es también una función de orden superior, toma una "función de prueba" como argumento. Juguemos con esta función y definamos algunas funciones más especializadas a partir de ella.

numOfEven xs = numOf even xs

Aquí definimos la función numOfEven que cuenta la cantidad de elementos pares en una lista. Notemos que no es necesario declarar explícitamente xs como un parámetro. Podríamos haber escrito numOfEven = numOf even. De hecho una definición muy clara. Pero por el momento, declararemos explícitamente los parámetros.

Ahora definamos una función que cuenta el número de elementos que son mayores o iguales a 5 :

numOfGE5 xs = numOf (>=5) xs

Aquí la función de prueba es simplemente ">=5" que se la pasamos a numOf para obtener la funcionalidad que necesitamos.

Ahora debieras ver que la modularidad de la programación funcional nos permite definir funciones genéricas donde parte de la funcionalidad se pasa como argumento, que luego podremos usar para definir nombres para cualquier función especializada. Este pequeño ejemplo es en cierta manera trivial, no debería ser muy difícil reescribir las definiciones de la funciones de más arriba, pero para funciones más complejas esto es muy práctico. Tu puedes, por ejemplo, escribir una función para recorrer un árbol binario auto-balanceado y tomar parte de la funcionalidad como parámetro (por ejemplo, la función de comparación). Esto te permitiría recorrer un árbol sobre cualquier tipo de datos, simplemente proveyendo la función de comparación que necesites. De esta manera puedes dedicar más esfuerzo en asegurarte que la función general es correcta, y después todas las funciones especializadas serán también correctas. Sin mencionar que no tendrás que copiar y pegar código en tu proyecto. Este concepto también es factible en lenguajes imperativos. En algunos lenguajes orientados a objetos uno tiene que proveer un "Objeto comparador" para árboles y otras estructuras de datos. La diferencia es que la forma à la Haskell es mucho más intuitiva y elegante (crear un tipo distinto sólo para comparar otros tipos y después pasar un objeto de este tipo no es una forma elegante de hacerlo), y por ello es más probable que sea usado frecuentemente (y no solo en bibliotecas estándares).

Un concepto central en los lenguajes funcionales es que el resultado de una función está determinada por su entrada, y sólo por su entrada. ¡No hay efectos laterales! Esto también se extiende a las variables - las variables en Haskell no varían. Esto puede sonar extraño si tu estas acostumbrado a la programación imperativa (donde la mayor parte del código consiste en cambiar el "contenido" de una variable), pero es realmente muy natural. Una variable en Haskell es un nombre al que se le da un valor; y no, como en los lenguajes imperartivos, una abstracción de algún concepto de bajo nivel como una celda de memoria . Cuando se piensa a las variables como atajos para valores (justo como en matemáticas), tiene sentido que no se permitan las modificaciones de las variables. Tu no esperarías que "4 = 5" sea una asignación válida en ningún lenguaje de programación; por lo tanto, es extraño que "x = 4; x = 5" esté permitido. Esto es díficil de captar para programadores muy acostumbrados a lenguajes imperativos, pero no es tan extraño como parece a primera vista. Por ello, cuando empieces a pensar cosas como "Esto es demasiado rebuscado, me vuelvo a C++!", trata de obligarte a continuar aprendiendo Haskell - estarás contento de haberlo hecho.

Eliminar efectos laterales de la ecuación permite que las expresiones sean evaluadas en cualquier orden. Una función puede devolver siempre el mismo resultado si recibe la misma entrada - sin excepciones. Este determinismo elimina toda una clase de errores encontrados en programas imperativos. De hecho, uno podría argumentar que la mayoría de los errores en sistemas grandes pueden ser atribuidos a efectos laterales - si no causados directamente por ellos, entonces causados por un diseño que se basa en efectos laterales. Esto significa que los programas funcionales tienden a tener muchos menos errores que los imperativos.

Conclusión

Como los lenguajes funcionales son más intuitivos y ofrecen más formas y formás más fáciles de hacer las cosas, los programas funcionales tienden a ser más cortos (usualmente entre 2 y 10 veces). La semántica está más cercana al problema que en una versión imperativa, lo que facilita la verificación de la corrección de una función. Más aun, Haskell no permite efectos laterales, esto lleva a tener menos errores. De esta manera, los programas en Haskell son más fáciles de escribir, más robustos y más fáciles de mantener.

Qué puede ofrecer Haskell al programador?

Haskell es un lenguaje moderno de propósito general desarrollado para incorporar el conocimiento colectivo de la comunidad de programación funcional en un lenguaje elegante, poderoso y general.

Pureza

A diferencia de otros lenguajes de programación funcional Haskell es puro. No permite ningún efecto lateral. Este es probablemente la característica más importante de Haskell. Ya hemos discutido brevemente los beneficios de la programación pura, libre de efectos laterales - y no hay mucho más que podamos decir sobre eso. Necesitas experimentarlo tu mismo.


Pereza

Otra característica de Haskell es que es perezoso (hablando técnicamente, esto es "no-estricto"). Esto significa que no se evalúa nada hasta tanto deba ser evaluado. Uno puede, por ejemplo, definir una lista infinita de primos sin caer en una recursión infinita. Sólo los elementos de esta lista que sean realmente usados serán computados. Esto permite algunas soluciones muy elegantes para muchos problemas. Un patrón típico de resolución de un problema sería definir una lista de todas las posibles soluciones y luego filtrar las ilegales. La lista resultante tendrá sólo soluciones legales. La evaluación perezosa hace esta operación muy limpia. Si solo se necesita una solución, simplemente se puede tomar el primer elemento de la lista resultante - la evaluación perezosa nos asegurará que nada más es evaluado innecesariamente.

Tipado fuerte

Más aun Haskell es fuertemente tipado, esto significa lo que dice. Es imposible convertir sin darse cuenta un Double a un Int, o seguir un puntero nulo. Esto también lleva a tener menos errores. Puede ser doloroso en los raros casos en que uno necesita convertir un Int a un Double explícitamente antes de hacer alguna operación, pero en la práctica eso no sucede muy a menudo como para convertirse en una molestia. De hecho, forzar cada conversión explícitamente puede ayudar a resaltar problemas en el código. En otros lenguajes donde estas conversiones son invisibles, los problemas surgen frecuentemente cuando el compilador trata un double como un entero o, aun peor, un entero como un puntero.

A diferencia de otros lenguajes fuertemente tipados, en Haskell los tipos son inferidos automáticamente. Esto significa que rara vez tendrás que declarar los tipos de tus funciones, excepto como forma de documentación. Haskell mirará cómo usas las variables y se dará cuenta de eso qué tipo debería tener la variable - después se hará un chequeo de tipos de todo para asegurarse que todos los tipos coinciden. Python tiene la noción de "duck typing", que significa "si camina y habla como un pato, entonces es un pato!". Se puede argumentar que Haskell tiene una forma mejor de "duck typing". Si un valor camina y habla como un pato, entonces será considerado un pato a través de la inferencia de tipos; pero, a diferencia de Python, el compilador también atrapará los errores si después intenta comportarse como un mono! Tu obtienes los beneficios de tipado fuerte (se capturan errores en tiempo de compilación, en vez de en tiempo de ejecución) sin las molestias que acarrea esto en otros lenguajes. Más aun, Haskell siempre inferirá el tipo más general para una variable. Por lo tanto, si tu escribes una función de ordenamiento sin una declaración de tipos, Haskell se asegurará que la función acepte cualquier valor que pueda ser odenado.

Compara esto con cómo lo harías en algún lenguaje orientado a objetos. Para tener polimorfismo, tendrías que usar alguna clase base, y después declarar tus variables como instancias de subclases de esa clase. Eso significa un montón de trabajo extra y declaraciones ridículamente complicadas sólo para declarar la existencia de una variable. Más aun, tendrías que realizar muchas conversiones de tipo a través de conversiones ("casts") explícitos - lo cual no es definitivamente una solución elegante. Si tu quieres escribir una función polimórfica en estos lenguajes orientados a objetos, probablemente declararías los parámetros como objetos de una clase base global (como "Object" en Java), que escencialmente permite le permite al programador pasarle cualquier cosa a la función, aun objetos que lógicamente no pueden ser pasados a la función. El resultado es que la mayoría de las funciones que escribes en esos lenguajes no son generales, sólo funcionan con un solo tipo de datos. Estás también corriendo el control de errores de tiempo de compilación a tiempo de ejecución. En sistemas grandes donde alguna funcionalidad se usa raramente, estos errores pueden no ser vistos hasta que causan un error fatal en el peor momento posible.

Haskell provee una form elegante, concisa y segura de escribir programas. Los programas no se colgarán inesperadamente, ni producirán basura extraña como salida.

Elegancia

Otra propiedad de Haskell que es muy importante para el programador, aun cuando no signifique mucho en términos de estabilidad o performance, es la elegancia. Para decirlo sencillamente: las cosas funcionan como tu te lo imaginas.

Para destacar la elegancia de Haskell repasaremos un pequeño ejemplo. Elejimos QuickSort porque es un algoritmo simple y realmente útil. Miraremos dos versiones - una escrita en C++, un lenguaje imperativo, y una escrita en Haskell. Ambas versiones sólo usan la funcionalidad disponible para el programador sin importar módulos extras (de otra forma podríamos simplemente invocar a la función "sort" de la biblioteca estándar de cada lenguaje y estaríamos hechos!). Entonces, vamos a usar las primitivas estándar de secuencias de cada lenguaje (una "lista" en Haskell y un "array" en C++). Ambas versiones deben ser polimórficas (lo que es hecho automáticamente por Haskell, y con templates en C++). Ambas versiones deben usar el mismo algoritmo recursivo. Por favor nota que esto no tiene la intención de ser una comparación definitiva entre los lenguajes. Sólo se quiere mostrar la elegancia de Haskell, la versión C++ sólo se incluye a los fines de la comparación (y podría haber sido codificada de otra manera si usaramos la Standard Template Library, por ejemplo).

template <typename T>
void qsort (T *result, T *list, int n)
{
    if (n == 0) return;
    T *smallerList, *largerList;
    smallerList = new T[n];
    largerList = new T[n];      
    T pivot = list[0];
    int numSmaller=0, numLarger=0;      
    for (int i = 1; i < n; i++)
        if (list[i] < pivot)
            smallerList[numSmaller++] = list[i];
        else 
            largerList[numLarger++] = list[i];
    
    qsort(smallerList,smallerList,numSmaller); 
    qsort(largerList,largerList,numLarger);
    
    int pos = 0;        
    for ( int i = 0; i < numSmaller; i++)
        result[pos++] = smallerList[i];
    
    result[pos++] = pivot;
    
    for ( int i = 0; i < numLarger; i++)
        result[pos++] = largerList[i];
    
    delete [] smallerList;
    delete [] largerList;
};

No explicaremos este código, sólo notemos cúan complejo y difícil es entenderlo en una mirada, principalmente porque el programador tiene que lidiar con detalles de bajo nivel que no tienen nada que ver con la tarea a resolver. Ahora veamos la versión de QuickSort en Haskell, que luce como sigue:

 qsort []     = []
 qsort (x:xs) = qsort less ++ [x] ++ qsort more
     where less = filter (<x)  xs
           more = filter (>=x) xs

Desglosemos este código en detalle, ya que usa mucha sintaxis de Haskell que puedes no conocer.

La función se llama qsort y toma una lista como su parámetro. Definimos una función en Haskell así: nombrefuncion a b c = expr, donde nombrefuncion es el nombre de la función, a, b, y c son los parámetros y expr es la expresión a ser evaluada (usualmente usando los parámetros). Las funciones se llaman simplemente escribiendo su nombre primero y luego su(s) parámetro(s). Haskell no usa paréntesis para la aplicación funcional. Las funciones simplemente tienen más precedencia que cualquier otra cosa, entonces "f 5 * 2", por ejemplo, aplicaría f a 5 y luego lo multiplicaría por 2; si quisieramos que la multiplicación ocurriera antes de la aplicación entonces usaríamos paréntesis como en "f (5*2)".

Volvamos al QuickSort. primero vemos que tenemos dos definiciones para la función. Esto se llama ajuste de patrones (pattern matching) y brevemente podemos decir que probará el argumento pasado a la función con los patrones desde arriba hacia abajo, usando el primero que se ajuste.

La primera definición se ajusta a [] que en Haskell es la lista vacía (una lista de 1, 2 y 3 es [1,2,3] así que tiene sentido que la lista vacía sean sólo dos corchetes). Así que cuando tratamos de ordenar la lista vacía, el resultado será una lista vacía. ¿Suena razonable, no?

El segundo patrón de definición se ajusta a una lista con al menos un elemento. Lo hace usando (x:xs) como su argumento. El operador "cons" es (:) y simplemente pone un elemento en frente de una lista, así 0 : [1,2,3] devuelve [0,1,2,3]. El ajuste de patrones contra (x:xs) se cumple con una lista con cabeza x y cola xs (que puede o no ser la lista vacía). En otras palabras, (x:xs) es una lista de al menos un elemento.

Así que como necesitaremos usar la cabeza de la lista después, podemos extraerla muy elegantemente mediante el ajuste de patrones. Puedes pensarlo como nombrar el contenido de la lista. Esto se puede hacer para cualquier construcción de datos, no sólo una lista. Es posible ajustar patrones con un nombre de variable arbitrario y luego usar la función head para obtener la cabeza de la lista.

Ahora si tenemos una lista no vacía, la lista ordenada se produce ordenando todos los elementos menores a x y poniéndolos en frente de x, y luego ordenando todos los elementos mayores a x y poniéndolos al final. Hacemos esto usando el operador de concatenación de listas ++. Nótese que x no es una lista así que el operador ++ no funcionará sobre ella, por lo que hacemos una lista unitaria al ponerla entre corchetes.

Así, la función se lee "Para ordenar la lista, ubica la cabeza entre la lista ordenada de todos los elementos menores a ella y la lista ordenada de todos los elementos mayores a ella". Esta frase podría bien ser la descripción original del algoritmo. Esto es muy común en Haskell. Una definición de función usualmente se asemeja mucho a la descripción informal de la función. Por esto es que decimos que Haskell tiene una brecha semántica menor que otros lenguajes.

Pero espera, ¡no hemos terminado todavía! ¿Cómo es que las listas less y more se calculan? Bueno, recuerda que no nos importa la secuenciación en Haskell, así que las hemos definido debajo de la función usando notación where (que permite usar los parámetros de la funcion en sus definiciones). Usamos la función del preludio estándar filter. No explicaremos demasiado esta parte, pero la línea less = filter (<x) xs usará filter (<x) xs para filtrar la lista xs. Puedes ver que realmente pasamos la función que se usará para filtrar la lista a filter, un ejemplo de funciones de alto orden.

La función (<x) debe leerse "la función 'menor a x'" y devolverá True si el elemento que se le pasa es menor que x (nótese qué fácil fue construir una función al vuelo, ponemos la expresión "<x", "menor a x", entre paréntesis y la mandamos a la función - ¡las funciones en realidad son sólo otro valor!).

Todos los elementos que pasan la prueba son la salida de filter y terminan en less. De la misma manera, (>=x) se usa para filtrar la lista por todos los elementos mayores o iguales a x.

Ahora que te han explicado la sintaxis, lee la definición de la función de nuevo. Nota qué poco tiempo toma lograr entender lo que la función hace. Las definiciones de funciones en Haskell explican lo que computan, no cómo lo hacen.

Si ya te has olvidado de la sintaxis esbozada arriba, ¡no te preocupes! La cubriremos de forma más completa y minuciosa en los tutoriales. Lo importante de este ejemplo es que el código Haskell es elegante e intuitivo.

Haskell y los errores

Hemos dicho varias veces que muchas características de Haskell ayudan a disminuir la ocurrencia de errores; recapitulemos esas características.

Los programas en Haskell tienen menos errores porque Haskell es:

  • Puro. No hay efectos laterales.
  • Fuertemente tipado. No puede haber uso dudoso de tipos. Y no hay core dumps!
  • Conciso. Los programas más cortos facilitan mirar una función y "captarla" de una sóla vez, convenciendote que es correcta.
  • Alto nivel. Los programas de Haskell casi siempre se escriben exactamente igual que la descripción del algoritmo. Lo que facilita verificar que la función haga lo que el algoritmo dice. Al codificar en un nivel de abstracción superior, dejando los detalles para el compilador, se deja menos espacios para que se deslicen errores.
  • Memoria administrada. No hay que preocuparse por los punteros, el Garbage Collector se ocupa de ellos. El programador puede preocuparse de la implementación del algoritmo, no de administración de memoria.
  • Modular. Haskell ofrece más "pegamento" y más fuerte para componer programas a partir de módulos ya desarrollados. De esta manera, los programas Haskell pueden ser más modulares. Frecuentemente se puede probar que el uso de funciones modulares es correcto por inducción. Combinar dos funciones que se probaron correctas dará un resultado correcto (asumiendo que la combinación es correcta).

Aun más, la mayoría de las personas concuerdan en que uno piensa diferente al resolver problemas en un lenguaje funcional. Uno subdivide el problema en funciones más y más pequeñas y luego uno escribe esas funciones pequeñas (y "garantizadas-de-corrección-casi-siempre"), que son compuestas de varias maneras para obtener el resultado final. ¡Simplemente no hay espacio para errores!

Haskell vs POO

El gran beneficio de la programación orientada a objetos (POO) no es que uno pueda agrupar los datos con las funciones que actúan sobre ellos en un objeto, el beneficio es que permite un buen encapsulamiento de datos (separando la interfaz de la implementación) y polimorfismo (permitiendo que un conjunto de tipo de datos se comporte de la misma manera). Sin embargo:

¡Encapsulamiento de datos y polimorfismo no son exclusivos de POO!

Haskell tiene herramientas para abstraer datos. No podemos meternos en ello sin haber visto el sistema de módulos y cómo funcionan los tipos abstractos de datos (TAD) en Haskell, algo que está bastante más allá del alcance de este ensayo. Por ello, haremos una corta descripción de cómo funcionan los TADs y el polimorfismo en Haskell.

El encapsulamiento en Haskell se logra declarando cada tipo de datos en un módulo independiente, y de este módulo sólo se exporta la interfaz. Internamente puede haber muchas funciones que manipulan los datos reales, pero la interfaz es lo único visible desde fuera del módulo. Notemos que el tipo de datos y las funciones que actúan sobre él no están agrupadas en un "objeto", sino que están (típicamente) agrupadas en el mismo módulo; de esta manera uno puede elegir exportar sólo ciertas funciones (y no los constructores para el tipo de datos), logrando así que estas funciones sean la única forma de manipular el tipo de datos - quedando "escondida" la implementación tras la interfaz.

El polimorfismo se logra usando lo que se llama clases de tipo. Si tu tienes conocimientos previos de C++ o Java, puedes asociar las clases con algo parecido a un template para construir un objeto, pero eso no es lo que son las clases en Haskell. Una clase de tipo es realmente eso a lo que suena. Es un conjunto de reglas para determinar si una instancia de un tipo es una instancia de la clase. Haskell separa la instanciación de la clase de la construcción del tipo de datos. Uno puede declarar un tipo "Porsche" como una instancia de la clase de tipo "Auto", digamos. Todas las funciones que pueden ser aplicadas a cualquier otro miembro de la clase de tipo Auto podrán ser aplicadas a un Porsche. Una clase que está incluida en Haskell es la clase de tipo Show, y un tipo puede ser instanciado en esa clase dando una función show, que convierta una instancia de ese tipo en un String. Consecuentemente, las instancias de casi todos los tipos de Haskell pueden ser mostradas en la pantalla aplicando la función show para convertirlos en un String, y usando después la acción de E/S correspondiente (hay más sobre E/S en los tutoriales).

Notemos cuán similar es esto con la noción de objeto en POO cuando se considera el aspecto del polimorfismo. El sistema de Haskell es más intuitivo para manejar el polimorfismo. Uno no necesita preocuparse en heredar en el orden correcto o asegurarse que la herencia tiene sentido. Una clase es simplemente una clase, y los tipos que son instancias de esta clase no tienen que compartir una relación de herencias padre-hijo. Si un tipo de datos cumple con los requisitos de una clase, entonces puede ser instanciada en esa clase. Simple, no? ¿Recuerdas el ejemplo de QuickSort? ¿Recuerdas que habíamos dicho que era polimórfico? El secreto detrás del polimorfismo en qsort es que está definida para trabajar sobre listas de cualquier tipo que pertenezca a la clase Ord (por "Ordenado"). Ord tiene definida un conjunto de funciones, entre ellas "<" y ">" que son suficientes para nuestras necesidades, porque sólo necesitamos saber si un elemento es mayor que x o no. Por ello si fueramos a definir las funciones que requiere Ord para nuestro tipo Porsche (sería suficiente implementar <= y ==, Haskell construiría el resto a partir de ellas) en una instanciación de la clase Ord, podríamos usar qsort para ordenar listas de Porsche (aun cuando no tenga sentido ordenar Porsches). Notemos que nunca dijimos nada sobre las clases a las que debían pertenecer los elementos de la lista, Haskell inferirá esto automáticamente viendo las funciones que hemos usado (en el ejemplo de qsort, solo "<" y ">=" son relevantes).

Para resumir: Haskell incluye mecanismos para encapsulamiento de datos que igualan o pasan a aquellos de los lenguajes orientados a objetos. Lo único que Haskell no provee es una forma de agrupar funciones y datos en un objeto (aparte de construir un tipo de datos que incluya una función, recuerda que las funciones son datos!). Sin embargo, esto es un problema menor: para aplicar una función a un objeto, escribiremos "func obj a b c" en vez de algo como "obj.func a b c".

Modularidad

Un concepto central en la informática es la modularidad. Una analogía popular es esta: digamos que tu quieres construir una silla de madera. Si tu construyes las partes por separado y luego las pegas, la tarea se resuelve fácilmente. Pero si tu tienes que tallar la silla entera a partir de un pedazo sólido de madera, te puede resultar un poco más difícil. John Hughes tiene algo para decir sobre el tema en este artículo: Why Functional Programming Matters (Por qué es importante la programación funcional)

"Los lenguajes que intenten mejorar la productividad deben permitir la programación modular. Pero nuevas reglas de alcance y mecanismos para compilación separada no son suficientes - modularidad significa más que módulos. Nuestra capacidad para descomponer problemas en partes depende directamente de nuestra capacidad para unir las soluciones. Para ayudar a la programación modular, un lenguaje deber proveer buena cola.

Los lenguajes de programación funcional proveen dos nuevos tipos de colas: funciones de alto orden y evaluación perezosa."

La velocidad de Haskell

Dejemos en claro que lo que sigue se aplica sólo en el caso general en el que la velocidad no es algo crítico en absoluto, donde se pueda aceptar tiempos de ejecución un poco más largos, si se reducen en gran medida los tiempos de desarrollos. Hay casos en los que la velocidad es de importancia primordial, en esos casos no se aplica esta sección en el mismo grado.

Algunos programadores de C++ pueden afirmar que la versión de QuickSort en C++ es probablemente un tanto más rápida que la versión en Haskell. Y esto puede ser cierto. Sin embargo, para la mayoría de las aplicaciones la diferencia en velocidad es tan pequeña que es completamente insignificante. Por ejemplo, mira Computer Language Shootout, donde Haskell logra resultados favorables respecto a la mayoría de los lenguajes llamados "rápidos". Si bien es cierto, que esas pruebas no prueban nada sobre la performance en el mundo real, sí muestran que Haskell no es tan lento como algunas personas piensan. Al momento de escribir esto está en la 2da posición, sólo levemente detrás de C (con C++ bastante más lejos).

Casi todos los programas en uso actualmente tienen una distribución bastante pareja en tiempo de procesamiento entre sus funciones. Las excepciones más notables son aplicaciones como encoders MPEG y conjuntos de pruebas artificiales, donde gran parte del tiempo de ejecución se pasa en pequeñas porciones del código. Si uno realmente necesita velocidad a cualquier costo, entonces se debe considerar usar C en vez de Haskell.

Hay una vieja regla en programación llamada la regla del "80/20". Esta regla dice que el 80% del tiempo se consume en 20% del código. La consecuencia de esto es que una función cualquiera de un sistema tiene una importancia mínima cuando se trata de optimización de velocidad. Puede haber un puñado de funciones suficientemente significativas a la hora de optimizar. Estas funciones podrían ser escritas en C (usando la excelente interfaz para funciones foráneas de Haskell). El rol que tiene ahora la programación en Assembler podría, y probablemente lo sea, ocupado por C - es decir, usándolo para las partes donde el tiempo es realmente crítico, pero no para el sistema entero.

Deberíamos seguir moviéndonos a niveles de abstracción más altos, como lo hicimos antes. Debemos cambiar la velocidad de las aplicaciones por mejores productividad, estabilidad y mantenibilidad. El tiempo de los programadores es casi siempre más caro que el tiempo de CPU. No escribimos más aplicaciones en Assembler por la misma razón por la que no deberíamos escribir más aplicaciones en C.

Finalmente recordemos que optimizar los algoritmos puede traer resultados mucho mejores que optimizar el código. Para ejemplos teóricos donde ciertos factores como el tiempo de desarrollo y la estabilidad no cuentan, entonces seguro que C es más rápido que Haskell. Pero en el mundo real, donde los tiempos de desarrollo importan, no es el caso. Si se puede desarrollar una aplicación en Haskell en la décima parte de lo que tomaría desarrollarla en C (la experiencia dice que esto no es tan poco común), entonces uno tendría mucho más tiempo para analizar e implementar nuevos algoritmos. Por ello, en el "mundo real" donde no tenemos infinita cantidad de tiempo para programar nuestras aplicaciones, los programas en Haskell pueden ser a menudo mucho más rápidos que los programas en C.

¿Por qué no es tan popular el Haskell como otros lenguajes de programación? Si el sistema operativo está escrito en C (o en algún otro lenguaje de programación imperativo) es muy probable que sea más fácil utilizar un lenguaje imperativo para interactuar con él. Otra posible razón es que los lenguajes de programación muy raramente se consideran herramientas intercambiables (a pesar de que lo son). Para la mayoría de la gente su lenguaje de programación preferido es similar a una religión; es difícil creer que puede existir otro lenguaje mejor y más rápido. Paul Graham escribió un documento llamado "Beating the Averages" ("Ganándole a los promedios") en el cual cuenta sus experiencias utilizando Lisp (otro lenguaje de programación funcional) en una empresa nueva. Graham utiliza una analogía a la que llama "La paradoja de Blub". Es algo así: suponiendo que el lenguaje de programación preferido de una persona sea "Blub" ("Blub" siendo un lenguaje de programación ficticio de poder medio), esta persona generalmente sólo podrá identificar lenguajes de programación de menor poder que "Blub". El programador entonces examina COBOL y piensa "¿Como puede alguien programar en COBOL, si no soporta X?" (tomando X como un característica que está presente en "Blub"). Sin embargo, esta persona tiene dificultades para juzgar lenguajes que están por encima de "Blub" en la escala. Si examina estos lenguajes, le parecerán "raros" porque el programador está "pensando en Blub" y no tiene posibilidad de comprender características avanzadas de lenguajes de programación más poderosos. Esto invariablemente lleva a la conclusión que para poder comparar todos los lenguajes uno debe estar en la cima de la escala de "poder". Yo creo que los lenguajes funcionales casi por definición están más cerca de la cima que los imperativos, así es que los lenguajes pueden limitar el campo de pensamiento de un programador. Si solamente has programado "Blub" puede que no veas sus limitaciones; se harán evidentes cambiando a un lenguaje más poderoso.

Haskell no se utiliza más a menudo porque la gente siente que "su" lenguaje hace "todo lo necesario"; y no se equivocan, ya que están pensando en "Blub". Pero los lenguajes de programación no son solamente tecnología, también son una forma de pensar; es muy difícil comprender la utilidad de Haskell si no se piensa en Haskell.

Con un poco de suerte, este artículo puede haberte ayudado a salir de la paradoja del "Blub". Aunque quizás todavía no "Pienses en Haskell", espero que al menos comiences a tomar conciencia de las limitaciones que impone en el pensamiento tu lenguaje de Introduprogramación "favorito" y que a partir de ahora tengas mayor motivación para expandir tus ideas. Si estás decidido a aprender un lenguaje funcional de programación para poder tener una vista mejor de la "escala de poder", entonces el Haskell es un candidato excelente.



Nota: Esta es una traducción del artículo original en Inglés : Why Haskell matters