Graças à avaliação preguiçosa, um programa Haskell não (quase não pode ) fazer o que parece que faz.
Considere este programa:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
Em uma linguagem ansiosa, primeiro quicksort
iria correr, então show
, então putStrLn
. Os argumentos de uma função são calculados antes que a função comece a ser executada.
Em Haskell, é o oposto. A função começa a funcionar primeiro. Os argumentos são calculados apenas quando a função realmente os usa. E um argumento composto, como uma lista, é calculado uma parte de cada vez, conforme cada parte dele é usada.
Então, a primeira coisa que acontece neste programa é queputStrLn
começa a funcionar.
A implementação do GHCputStrLn
funciona copiando os caracteres do argumento String para um buffer de saída. Mas quando ele entra neste loop, show
ainda não foi executado. Portanto, quando vai copiar o primeiro caractere da string, Haskell avalia a fração de show
e as quicksort
chamadas necessárias para calcular esse caractere . Em seguida, putStrLn
passa para o próximo personagem. Portanto, a execução de todas as três funções putStrLn
- show
, e quicksort
- é intercalada. quicksort
executa de forma incremental, deixando um gráfico de thunks não avaliados à medida que vai para lembrar onde parou.
Agora, isso é totalmente diferente do que você pode esperar se estiver familiarizado com, você sabe, qualquer outra linguagem de programação. Não é fácil visualizar como quicksort
realmente se comporta em Haskell em termos de acessos à memória ou mesmo a ordem das comparações. Se você pudesse apenas observar o comportamento, e não o código-fonte, não reconheceria o que ele está fazendo como um quicksort .
Por exemplo, a versão C do quicksort particiona todos os dados antes da primeira chamada recursiva. Na versão Haskell, o primeiro elemento do resultado será calculado (e pode até aparecer em sua tela) antes que a execução da primeira partição termine - na verdade, antes que qualquer trabalho seja concluído greater
.
PS O código Haskell seria mais semelhante a quicksort se fizesse o mesmo número de comparações que quicksort; o código conforme escrito faz o dobro de comparações porque lesser
e greater
são especificados para serem calculados independentemente, fazendo duas varreduras lineares na lista. É claro que, em princípio, é possível que o compilador seja inteligente o suficiente para eliminar as comparações extras; ou o código pode ser alterado para uso Data.List.partition
.
PPS O exemplo clássico de algoritmos Haskell que não se comportam como você esperava é a peneira de Eratóstenes para computar os primos.