Por que a avaliação preguiçosa não é usada em todos os lugares?

32

Acabei de aprender como funciona a avaliação lenta e me perguntei: por que a avaliação lenta não é aplicada em todos os softwares atualmente produzidos? Por que ainda está usando avaliação ansiosa?

John Smith
fonte
2
Aqui está um exemplo do que pode acontecer se você misturar estado mutável e avaliação lenta. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström 15/03
2
@ JonasElfström: Por favor, não confunda estado mutável com uma de suas possíveis implementações. O estado mutável pode ser implementado usando um fluxo infinito e lento de valores. Então você não tem o problema de variáveis ​​mutáveis.
Giorgio
Nas linguagens de programação imperativas, a "avaliação lenta" requer um esforço consciente do programador. A programação genérica em linguagens imperativas tornou isso fácil, mas nunca será transparente. A resposta para o outro lado da pergunta traz à tona outra questão: "Por que as linguagens de programação funcional não são usadas em todos os lugares?", E a resposta atual é simplesmente "não" na atualidade.
rwong
2
Linguagens de programação funcional não são usadas em todos os lugares pela mesma razão que não usamos martelos em parafusos, nem todo problema pode ser facilmente expresso de uma forma funcional de entrada -> saída, a GUI, por exemplo, é mais adequada para ser expressa de maneira imperativa .
ALXGTV
Além disso, existem duas classes de linguagens de programação funcional (ou pelo menos ambas afirmam ser funcionais), as linguagens funcionais imperativas, por exemplo, Clojure, Scala e as declarativas, por exemplo, Haskell, OCaml.
ALXGTV

Respostas:

38

A avaliação preguiçosa exige despesas gerais de contabilidade - você precisa saber se já foi avaliado e essas coisas. A avaliação ansiosa é sempre avaliada, para que você não precise saber. Isto é especialmente verdade em contextos concorrentes.

Em segundo lugar, é trivial converter a avaliação ansiosa em avaliação lenta, empacotando-a em um objeto de função a ser chamado posteriormente, se você desejar.

Em terceiro lugar, a avaliação preguiçosa implica perda de controle. E se eu avaliasse preguiçosamente a leitura de um arquivo de um disco? Ou conseguir tempo? Isso não é aceitável.

A avaliação ansiosa pode ser mais eficiente e controlável e é trivialmente convertida em avaliação lenta. Por que você quer uma avaliação preguiçosa?

DeadMG
fonte
10
A leitura lenta de um arquivo do disco é realmente muito interessante - para a maioria dos meus programas e scripts simples, o Haskell's readFileé exatamente o que eu preciso. Além disso, a conversão da avaliação preguiçosa para a ansiosa é igualmente trivial.
Tikhon Jelvis 12/12
3
Concordo com todos, exceto o último parágrafo. Avaliação preguiçosa é mais eficiente quando há uma operação da cadeia, e ele pode ter mais controle de quando você realmente precisa dos dados
texasbruce
4
As leis do functor gostariam de ter uma palavra com você sobre "perda de controle". Se você escreve funções puras que operam em tipos de dados imutáveis, a avaliação lenta é uma dádiva de Deus. Idiomas como haskell são fundamentalmente baseados no conceito de preguiça. É complicado em alguns idiomas, especialmente quando misturado com código "inseguro", mas você está fazendo parecer que a preguiça é perigosa ou ruim por padrão. É apenas "perigoso" no código perigoso.
Sara
1
@DeadMG Não, se você se importa se o seu código termina ou não ... O que head [1 ..]você fornece em uma linguagem pura avaliada com avidez, porque em Haskell ele fornece 1?
ponto
1
Para muitos idiomas, a implementação de uma avaliação lenta introduzirá, no mínimo, complexidade. Às vezes, essa complexidade é necessária e a avaliação preguiçosa melhora a eficiência geral - principalmente se o que está sendo avaliado for apenas condicionalmente necessário. No entanto, mal feito, pode introduzir erros sutis ou difíceis de explicar problemas de desempenho devido a suposições ruins ao escrever o código. Há uma troca.
Berin Loritsch
17

Principalmente porque código e estado preguiçosos podem se misturar mal e causar alguns erros difíceis de encontrar. Se o estado de um objeto dependente mudar, o valor do seu objeto lento pode estar errado quando avaliado. É muito melhor que o programador codifique explicitamente o objeto como preguiçoso quando souber que a situação é apropriada.

Em uma nota lateral, Haskell usa a avaliação Lazy para tudo. Isso é possível porque é uma linguagem funcional e não usa estado (exceto em algumas circunstâncias excepcionais em que elas estão claramente marcadas)

Tom Squires
fonte
Sim, estado mutável + avaliação preguiçosa = morte. Acho que os únicos pontos que perdi na final do SICP foram sobre o uso set!de um intérprete preguiçoso do Scheme. > :(
Tikhon Jelvis
3
"código e estado lento podem se misturar mal": Depende realmente de como você implementa o estado. Se você implementá-lo usando variáveis ​​mutáveis ​​compartilhadas e depender da ordem da avaliação para que seu estado seja consistente, você está certo.
Giorgio
14

A avaliação preguiçosa nem sempre é melhor.

Os benefícios de desempenho da avaliação lenta podem ser ótimos, mas não é difícil evitar a maioria das avaliações desnecessárias em ambientes ansiosos - certamente a preguiça a torna fácil e completa, mas raramente a avaliação desnecessária no código é um grande problema.

O lado bom da avaliação preguiçosa é quando ela permite escrever um código mais claro; obter o 10º primo filtrando uma lista de números naturais infinitos e pegar o 10º elemento dessa lista é uma das formas mais concisas e claras de proceder: (pseudocódigo)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Eu acredito que seria bastante difícil expressar as coisas de forma tão concisa sem preguiça.

Mas a preguiça não é a resposta para tudo. Para iniciantes, a preguiça não pode ser aplicada de forma transparente na presença de estado, e acredito que a condição de estado não pode ser detectada automaticamente (a menos que você esteja trabalhando, digamos, Haskell, quando o estado é bastante explícito). Portanto, na maioria dos idiomas, a preguiça precisa ser feita manualmente, o que torna as coisas menos claras e, portanto, remove um dos grandes benefícios da avaliação preguiçosa.

Além disso, a preguiça tem desvantagens de desempenho, pois incorre em uma sobrecarga significativa de manter expressões não avaliadas por perto; eles usam o armazenamento e são mais lentos para trabalhar do que os valores simples. Não é incomum descobrir que você precisa ter um código ansioso, porque a versão lenta é lenta - e às vezes é difícil pensar em desempenho.

Como costuma acontecer, não existe uma melhor estratégia absoluta. O Lazy é ótimo se você pode escrever um código melhor aproveitando infinitas estruturas de dados ou outras estratégias que ele permite que você use, mas o ansioso pode ser mais fácil de otimizar.

alex
fonte
Seria possível para um compilador realmente inteligente mitigar significativamente a sobrecarga. ou tirar proveito da preguiça para otimizações extras?
Tikhon Jelvis 12/12
3

Aqui está uma pequena comparação dos prós e contras da avaliação ansiosa e preguiçosa:

  • Avaliação ansiosa:

    • Potencial sobrecarga de avaliar coisas desnecessariamente.

    • Avaliação rápida e sem obstáculos.

  • Avaliação preguiçosa:

    • Nenhuma avaliação desnecessária.

    • Sobrecarga da contabilidade a cada uso de um valor.

Portanto, se você tem muitas expressões que nunca precisam ser avaliadas, a preguiça é melhor; No entanto, se você nunca tiver uma expressão que não precise ser avaliada, a preguiça é pura sobrecarga.

Agora, vamos dar uma olhada no software do mundo real: Quantas das funções que você escreve não requerem avaliação de todos os seus argumentos? Especialmente com as funções curtas modernas que apenas fazem uma coisa, a porcentagem de funções se enquadra nessa categoria é muito baixa. Assim, uma avaliação preguiçosa apenas introduziria a sobrecarga da contabilidade na maioria das vezes, sem a chance de realmente salvar alguma coisa.

Consequentemente, a avaliação preguiçosa simplesmente não compensa, em média, a avaliação ansiosa é a melhor opção para o código moderno.

cmaster
fonte
1
"Sobrecarga de contabilidade a cada uso de um valor.": Não acho que a sobrecarga de contabilidade seja maior do que, digamos, verificar referências nulas em uma linguagem como Java. Nos dois casos, você precisa verificar um bit de informação (avaliado / pendente versus nulo / não nulo) e precisa fazê-lo sempre que usar um valor. Então, sim, há uma sobrecarga, mas é mínima.
Giorgio
1
"Quantas das funções que você escreve não requerem avaliação de todos os seus argumentos?": Este é apenas um exemplo de aplicativo. E as estruturas de dados infinitas e recursivas? Você pode implementá-los com uma avaliação ansiosa? Você pode usar iteradores, mas a solução nem sempre é tão concisa. É claro que você provavelmente não perde algo que nunca teve a chance de usar extensivamente.
Giorgio
2
"Consequentemente, a avaliação preguiçosa simplesmente não paga em média, a avaliação ansiosa é a melhor opção para o código moderno.": Esta afirmação não se aplica: ela realmente depende do que você está tentando implementar.
Giorgio
1
@Giorgio A sobrecarga pode não parecer muito para você, mas os condicionais são uma das coisas que as CPUs modernas sugam: Uma ramificação imprevisível geralmente força uma liberação completa do pipeline, descartando o trabalho de mais de dez ciclos de CPU. Você não deseja condições desnecessárias no seu loop interno. Pagar dez ciclos extras por argumento de função é quase tão inaceitável para código sensível ao desempenho quanto codificar a coisa em java. Você está certo de que a avaliação preguiçosa permite realizar alguns truques que você não pode fazer facilmente com uma avaliação ansiosa. Mas a grande maioria do código não precisa desses truques.
C7
2
Esta parece ser uma resposta da inexperiência com idiomas com avaliação lenta. Por exemplo, e as estruturas de dados infinitas?
Andres F.
3

Como observou o @DeadMG, a avaliação preguiçosa exige despesas gerais de contabilidade. Isso pode ser caro em relação à avaliação ansiosa. Considere esta declaração:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Isso levará um pouco de cálculo para calcular. Se eu uso uma avaliação lenta, preciso verificar se ela foi avaliada toda vez que a uso. Se isso estiver dentro de um circuito fechado muito usado, a sobrecarga aumentará significativamente, mas não haverá benefícios.

Com uma avaliação ágil e um compilador decente, a fórmula é calculada em tempo de compilação. A maioria dos otimizadores retirará a atribuição de quaisquer loops em que ocorra, se apropriado.

A avaliação preguiçosa é mais adequada para carregar dados que serão acessados ​​com pouca frequência e possuem uma alta sobrecarga para recuperar. Portanto, é mais apropriado desviar casos do que a funcionalidade principal.

Em geral, é uma boa prática avaliar as coisas que são freqüentemente acessadas o mais cedo possível. A avaliação preguiçosa não funciona com essa prática. Se você sempre acessará algo, tudo o que a avaliação preguiçosa fará é adicionar sobrecarga. O custo / benefício do uso da avaliação lenta diminui à medida que o item que está sendo acessado se torna menos provável.

Sempre usar a avaliação lenta também implica em otimização antecipada. Essa é uma prática ruim que geralmente resulta em código muito mais complexo e caro que, de outra forma, poderia ser o caso. Infelizmente, a otimização prematura geralmente resulta em código com desempenho mais lento que o mais simples. Até que você possa medir o efeito da otimização, é uma má idéia otimizar seu código.

Evitar a otimização prematura não entra em conflito com as boas práticas de codificação. Se não foram aplicadas boas práticas, as otimizações iniciais podem consistir na aplicação de boas práticas de codificação, como mover cálculos para fora dos loops.

BillThor
fonte
1
Você parece estar argumentando por inexperiência. Sugiro que você leia o artigo "Por que a programação funcional é importante", de Wadler. Ele dedica uma seção importante que explica o porquê da avaliação lenta (dica: tem pouco a ver com desempenho, otimização antecipada ou "carregamento de dados acessados ​​com pouca frequência" e tudo a ver com modularidade).
Andres F.
@AndresF Eu li o artigo a que você se refere. Eu concordo com o uso de avaliação lenta nesses casos. A avaliação antecipada pode não ser apropriada, mas eu argumentaria que o retorno da subárvore para o movimento selecionado pode ter um benefício significativo se movimentos adicionais puderem ser adicionados facilmente. No entanto, criar essa funcionalidade pode ser uma otimização prematura. Fora da programação funcional, tenho problemas significativos importantes com o uso da avaliação lenta e a falha em usar a avaliação lenta. Há relatos de custos significativos de desempenho resultantes de uma avaliação lenta na programação funcional.
BillThor
2
Tal como? Há relatos de custos significativos de desempenho ao usar a avaliação ágil (custos na forma de avaliação desnecessária, bem como não rescisão do programa). Existem custos para quase qualquer outro recurso (mal) usado, pense nisso. A própria modularidade pode ter um custo; a questão é se vale a pena.
Andres F.
3

Se precisarmos avaliar completamente uma expressão para determinar seu valor, a avaliação lenta pode ser uma desvantagem. Digamos que temos uma longa lista de valores booleanos e queremos descobrir se todos eles são verdadeiros:

[True, True, True, ... False]

Para fazer isso, precisamos examinar todos os elementos da lista, não importa o quê, para que não haja possibilidade de interromper a avaliação preguiçosamente. Podemos usar uma dobra para determinar se todos os valores booleanos da lista são verdadeiros. Se usarmos uma dobra à direita, que usa avaliação lenta, não obtemos nenhum dos benefícios da avaliação lenta porque precisamos examinar todos os elementos da lista:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Uma dobra à direita será muito mais lenta nesse caso do que uma dobra estrita à esquerda, que não usa avaliação lenta:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

O motivo é que uma dobra estrita à esquerda usa recursão de cauda, ​​o que significa que acumula o valor de retorno e não acumula e armazena na memória uma grande cadeia de operações. Isso é muito mais rápido que a dobra preguiçosa à direita, porque ambas as funções precisam olhar para toda a lista e a dobra à direita não pode usar a recursão da cauda. Portanto, o ponto é que você deve usar o que for melhor para a tarefa em questão.

tail_recursion
fonte
"Então, o ponto é que você deve usar o que for melhor para a tarefa em questão." +1
Giorgio