Estou ciente de que a dobra para a esquerda produz árvores inclinadas para a esquerda e a dobra para a direita produz árvores com inclinação para a direita, mas quando procuro uma dobra, às vezes me encontro atolado em pensamentos que induzem a dor de cabeça tentando determinar que tipo de dobra é apropriado. Eu geralmente acabo resolvendo todo o problema e percorrendo a implementação da função de dobra conforme se aplica ao meu problema.
Então, o que eu quero saber é:
- Quais são algumas regras básicas para determinar se dobrar para a esquerda ou direita?
- Como posso decidir rapidamente que tipo de dobra usar, considerando o problema que estou enfrentando?
Há um exemplo em Scala por Exemplo (PDF) de usar uma dobra para escrever uma função chamada nivelar, que concatena uma lista de listas de elementos em uma única lista. Nesse caso, uma dobra certa é a escolha adequada (dada a forma como as listas são concatenadas), mas tive que pensar um pouco sobre isso para chegar a essa conclusão.
Visto que dobrar é uma ação comum na programação (funcional), gostaria de poder tomar esse tipo de decisão com rapidez e confiança. Então ... alguma dica?
Respostas:
Você pode transferir uma dobra para uma notação de operador infixo (escrevendo no meio):
Este exemplo dobra usando a função de acumulador
x
assim é igual
Agora você só precisa raciocinar sobre a associatividade do seu operador (colocando parênteses!).
Se você tiver um operador associativo à esquerda , você definirá os parênteses como este
Aqui, você usa uma dobra esquerda . Exemplo (pseudocódigo estilo haskell)
Se o seu operador for associativo à direita ( dobra direita ), os parênteses seriam definidos assim:
Exemplo: Cons-Operator
Em geral, os operadores aritméticos (a maioria dos operadores) são associativos à esquerda, portanto,
foldl
são mais difundidos. Mas nos outros casos, notação infixa + parênteses é bastante útil.fonte
foldl1
efoldr1
em Haskell (foldl
efoldr
assume um valor inicial externo), e os "contras" de Haskell(:)
não são chamados(::)
, mas caso contrário, está correto. Você pode querer adicionar que Haskell fornece adicionalmente umfoldl'
/foldl1'
que são variantes estritas defoldl
/foldl1
, porque aritmética preguiçosa nem sempre é desejável.Olin Shivers os diferenciou dizendo "foldl é o iterador fundamental da lista" e "foldr é o operador fundamental de recursão da lista". Se você observar como o foldl funciona:
você pode ver o acumulador (como em uma iteração recursiva de cauda) sendo construído. Em contraste, foldr continua:
onde você pode ver o percurso até o caso base 4 e construir o resultado a partir daí.
Portanto, proponho uma regra prática: se parece uma iteração de lista, uma que seria simples de escrever na forma recursiva de cauda, foldl é o caminho a seguir.
Mas, na verdade, isso provavelmente ficará mais evidente pela associatividade dos operadores que você está usando. Se eles forem associativos à esquerda, use foldl. Se eles forem associativos à direita, use foldr.
fonte
Outros pôsteres deram boas respostas e não vou repetir o que já disseram. Como você deu um exemplo de Scala em sua pergunta, darei um exemplo específico de Scala. Como Tricks já disse, a
foldRight
precisa preservar osn-1
frames da pilha, onden
está o comprimento da sua lista e isso pode facilmente levar a um estouro de pilha - e nem mesmo a recursão de cauda poderia salvá-lo disso.A
List(1,2,3).foldRight(0)(_ + _)
seria reduzido a:enquanto
List(1,2,3).foldLeft(0)(_ + _)
reduziria para:que pode ser calculado iterativamente, como feito na implementação de
List
.Em uma linguagem estritamente avaliada como Scala, um
foldRight
pode facilmente explodir a pilha de listas grandes, enquanto umfoldLeft
não.Exemplo:
Minha regra é, portanto - para operadores que não têm uma associatividade específica, sempre use
foldLeft
, pelo menos em Scala. Caso contrário, siga os outros conselhos fornecidos nas respostas;).fonte
Também é importante notar (e eu percebo que isso está afirmando um pouco o óbvio), no caso de um operador comutativo os dois são praticamente equivalentes. Nesta situação, um foldl pode ser a melhor escolha:
foldl:
(((1 + 2) + 3) + 4)
pode calcular cada operação e transportar o valor acumulado para a frentefoldr:
(1 + (2 + (3 + 4)))
precisa que um frame de pilha seja aberto para1 + ?
e2 + ?
antes de calcular3 + 4
, então ele precisa voltar e fazer o cálculo para cada um.Não sou especialista o suficiente em linguagens funcionais ou otimizações de compiladores para dizer se isso realmente fará diferença, mas certamente parece mais limpo usar um foldl com operadores comutativos.
fonte
foldr
pode muito bem ser mais eficiente do quefoldl
e não exigirá nenhum quadro de pilha extra.