Como fold
as diferenças parecem ser uma fonte frequente de confusão, então aqui está uma visão geral:
Considere dobrar uma lista de n valores [x1, x2, x3, x4 ... xn ]
com alguma função f
e semente z
.
foldl
é:
- Associativo esquerdo :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Cauda recursiva : itera pela lista, produzindo o valor posteriormente
- Preguiçoso : nada é avaliado até que o resultado seja necessário
- Para trás :
foldl (flip (:)) []
inverte uma lista.
foldr
é:
- Associativo certo :
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- Recursivo em um argumento : cada iteração se aplica
f
ao próximo valor e ao resultado de dobrar o restante da lista.
- Preguiçoso : nada é avaliado até que o resultado seja necessário
- Encaminha :
foldr (:) []
retorna uma lista inalterada.
Há um ponto ligeiramente sutil aqui que as viagens de pessoas até às vezes: Porque foldl
é para trás de cada aplicação de f
é adicionada ao lado de fora do resultado; e por ser preguiçoso , nada é avaliado até que o resultado seja necessário. Isso significa que, para calcular qualquer parte do resultado, Haskell primeiro percorre toda a lista, construindo uma expressão de aplicativos de funções aninhadas, depois avalia a função mais externa , avaliando seus argumentos conforme necessário. Se f
sempre usa seu primeiro argumento, isso significa que Haskell deve recorrer até o termo mais interno e, em seguida, trabalhar com computação inversa de cada aplicativo f
.
Obviamente, isso está muito longe da eficiente recursão da cauda que a maioria dos programadores funcionais conhece e ama!
De fato, embora foldl
seja tecnicamente recursivo da cauda, porque toda a expressão do resultado é criada antes de avaliar qualquer coisa, foldl
pode causar um estouro de pilha!
Por outro lado, considere foldr
. Também é preguiçoso, mas, como é executado em frente , cada aplicativo de f
é adicionado à parte interna do resultado. Portanto, para calcular o resultado, Haskell constrói um aplicativo de função única , cujo segundo argumento é o restante da lista dobrada. Se f
for preguiçoso em seu segundo argumento - um construtor de dados, por exemplo - o resultado será incrementalmente preguiçoso , com cada etapa da dobra calculada apenas quando parte do resultado que precisa dele for avaliada.
Assim, podemos ver por que foldr
às vezes funciona em listas infinitas quando foldl
não funciona: a primeira pode converter uma lista infinitamente preguiçosamente em outra estrutura de dados infinita lenta, enquanto a segunda deve inspecionar a lista inteira para gerar qualquer parte do resultado. Por outro lado, foldr
com uma função que precisa de ambos os argumentos imediatamente, como (+)
, funciona (ou melhor, não funciona) foldl
, criando uma expressão enorme antes de avaliá-la.
Portanto, os dois pontos importantes a serem observados são os seguintes:
foldr
pode transformar uma estrutura de dados recursiva lenta em outra.
- Caso contrário, as dobras preguiçosas travarão com um estouro de pilha em listas grandes ou infinitas.
Você deve ter notado que parece que foldr
pode fazer tudo o que foldl
pode, além de mais. Isso é verdade! De fato, foldl é quase inútil!
Mas e se quisermos produzir um resultado não preguiçoso dobrando uma lista grande (mas não infinita)? Para isso, queremos uma dobra estrita , fornecida pelas bibliotecas padrão :
foldl'
é:
- Associativo esquerdo :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Cauda recursiva : itera pela lista, produzindo o valor posteriormente
- Estrito : cada aplicativo de função é avaliado ao longo do caminho
- Para trás :
foldl' (flip (:)) []
inverte uma lista.
Por foldl'
ser rigoroso , para calcular o resultado que Haskell avaliará f
a cada etapa, em vez de deixar o argumento da esquerda acumular uma expressão enorme e não avaliada. Isso nos dá a recursão da cauda usual e eficiente que queremos! Em outras palavras:
foldl'
pode dobrar listas grandes com eficiência.
foldl'
travará em um loop infinito (não causará um estouro de pilha) em uma lista infinita.
O wiki da Haskell também tem uma página discutindo isso .
foldr
é melhor do quefoldl
em Haskell , enquanto o oposto é verdadeiro em Erlang (que aprendi antes de Haskell ). Desde Erlang não é preguiçoso e funções não são curry , por isso,foldl
em Erlang se comporta comofoldl'
acima. Esta é uma ótima resposta! Bom trabalho e obrigado!