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!foldl
"atrasada" efoldr
"encaminhada" problemática. Isso ocorre em parte porqueflip
está sendo aplicado(:)
na ilustração de por que a dobra está para trás. A reação natural é: "é claro que é para trás: você podeflip
listar a concatenação!" Também é estranho ver o chamado "backward", uma vez quefoldl
se aplicaf
ao primeiro elemento da lista primeiro (mais interno) em uma avaliação completa. Éfoldr
que "retrocede", aplicando-f
se ao último elemento primeiro.foldl
efoldr
ignorar rigor e otimizações, primeiro significa "mais externo", não "mais interno". É por isso quefoldr
pode processar listas infinitas efoldl
não pode - a dobra direita se aplica primeirof
ao primeiro elemento da lista e o resultado (não avaliado) de dobrar a cauda, enquanto a dobra esquerda deve percorrer a lista inteira para avaliar a aplicação mais externaf
.last xs = foldl (\a z-> z) undefined xs
.etc.
Intuitivamente,
foldl
está sempre do lado de fora ou do lado esquerdo, para que seja expandido primeiro. Ao infinito.fonte
Você pode ver na documentação de Haskell aqui que foldl é recursivo de cauda e nunca terminará se for passado uma lista infinita, já que se chama o próximo parâmetro antes de retornar um valor ...
fonte
Não conheço Haskell, mas em Scheme
fold-right
sempre 'atuará' no último elemento de uma lista primeiro. Assim, não funcionará para a lista cíclica (que é igual a uma infinita).Não tenho certeza se
fold-right
pode ser escrito com recursividade de cauda, mas para qualquer lista cíclica você deve obter um estouro de pilha.fold-left
Normalmente, o OTOH é implementado com recursão de cauda e fica preso em um loop infinito, se não for finalizado cedo.fonte