Gracias a la evaluación perezosa, un programa Haskell no hace (casi no puede ) hacer lo que parece.
Considere este programa:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
En un lenguaje ávido, primero quicksort
correría, luego show
, luego putStrLn
. Los argumentos de una función se calculan antes de que esa función comience a ejecutarse.
En Haskell, es todo lo contrario. La función comienza a ejecutarse primero. Los argumentos solo se calculan cuando la función realmente los usa. Y un argumento compuesto, como una lista, se calcula una pieza a la vez, a medida que se utiliza cada pieza.
Entonces, lo primero que sucede en este programa es queputStrLn
comienza a ejecutarse.
La implementación de GHC deputStrLn
funciona copiando los caracteres del argumento String en un búfer de salida. Pero cuando entra en este bucle, show
aún no se ha ejecutado. Por lo tanto, cuando va a copiar el primer carácter de la cadena, Haskell evalúa la fracción de show
y las quicksort
llamadas necesarias para calcular ese carácter . Luego putStrLn
pasa al siguiente personaje. Por lo que la ejecución de los tres funciones- putStrLn
, show
y quicksort
- se intercalan. quicksort
se ejecuta de forma incremental, dejando un gráfico de procesadores no evaluados a medida que avanza para recordar dónde se quedó.
Ahora bien, esto es tremendamente diferente de lo que podría esperar si está familiarizado con, ya sabe, cualquier otro lenguaje de programación. No es fácil visualizar cómo se quicksort
comporta realmente Haskell en términos de accesos a la memoria o incluso el orden de las comparaciones. Si solo pudiera observar el comportamiento, y no el código fuente, no reconocería lo que está haciendo como una clasificación rápida .
Por ejemplo, la versión C de quicksort particiona todos los datos antes de la primera llamada recursiva. En la versión Haskell, el primer elemento del resultado se calculará (e incluso podría aparecer en su pantalla) antes de que termine de ejecutarse la primera partición, de hecho, antes de que se realice ningún trabajo greater
.
PD: El código de Haskell sería más parecido a una ordenación rápida si hiciera el mismo número de comparaciones que la ordenación rápida; el código tal como está escrito hace el doble de comparaciones porque lesser
y greater
se especifican para ser calculados de forma independiente, haciendo dos escaneos lineales a través de la lista. Por supuesto, en principio es posible que el compilador sea lo suficientemente inteligente como para eliminar las comparaciones adicionales; o el código podría cambiarse para usarData.List.partition
.
PPS El ejemplo clásico de que los algoritmos de Haskell no se comportan como se esperaba es el tamiz de Eratóstenes para calcular números primos.