Sobre folds e uma pegadinha
por Hercules Lemke Merscher
Em Haskell uma maneira bem comum de iterar sobre uma lista e acumular valores é por meio de recursão. Geralmente usamos pattern matching para tratar o caso da lista vazia, seguido do caso da lista com um ou mais valores, aplicando a função e chamando a função novamente com o restante da lista.
Na medida que vamos nos habituando com recursão esse tipo de função se torna comum em código Haskell. Por se tratar de um padrão muito comum, algumas funções foram incluídas junto da linguagem para abstrair o uso de recursão, e essas funções são chamadas folds.
Uma função fold recebe 3 parâmetros, a função acumuladora, um valor inicial e a lista que será acumulada.
foldr
Acumulando uma lista com a função foldr:
Manipular pequenas listas não será um problema, agora imagine uma lista com 1000000000 itens. Para cada item a função + adicionará um item a pilha de execução enquanto aguarda o resultado da execução seguinte do foldr, até que um estouro de pilha aconteça.
O problema com a função foldr é que os resultados intermediários poderiam ser acumulados antes da próxima chamada, o que não é feito, como deixa claro no exemplo acima.
foldl
Para resolver este problema temos a função foldl, que vai funcionar de maneira análoga, mas com uma diferença sutil, vai acumular os itens de cada etapa até o final, o que evita milhares de itens sendo adicionados a pilha de execução. Seria algo assim:
Executando o foldl em uma lista com muitos valores:
WTF! Se está acumulando os valores intermediários, porque diabos está acontecendo um estouro de pilha?
Rá! Pegadinha do malandro.
Devemos lembrar que Haskell é uma linguagem com avaliação preguiçosa, e só avalia uma expressão quando esta realmente é necessária para retornar um valor, por conta disso os valores não estavam sendo acumulados de fato. Vamos ao passo a passo de execução usando foldl:
Ao invés de acumular os valores, os mesmos agora estão sendo alocados no heap, que é limitado pela quantidade de memória e swap presente na máquina, o que acaba causando o um stack overflow quando não há mais espaço para alocação. Triste, não? :(
É um comportamento inesperado que surpreende a primeira vez que nos deparamos com isso. Mas então, como resolver isso?
foldl’
Temos de forçar a avaliação dos acumuladores para evitar este problema, e é exatamente o que a função foldl’ faz, utilizando por debaixo dos panos a função seq:
A função seq é uma função que recebe 2 parâmetros, avalia o primeiro e retorna o segundo, perfeito para resolver o problema do foldl. Com o acumulador sendo avaliado forçadamente em cada passo, não temos alocações exageradas no heap, com isso evita-se o estouro de pilha.
Conclusão
Para a maioria dos casos foldr será suficiente, funcionando mesmo com listas infinitas. Para os outros casos quando a lista manipulada for bem grande e finita, foldl’ será a opção. Dado os motivos já discutidos anteriormente neste post, é melhor evitar o foldl.
Para mais detalhes, consulte a wiki do site haskell.org em inglês: Foldr_Foldl_Foldl’