Por que o conceito de avaliação preguiçosa é útil?


30

Parece que a avaliação preguiçosa de expressões pode fazer com que um programador perca o controle sobre a ordem em que seu código é executado. Estou tendo problemas para entender por que isso seria aceitável ou desejado por um programador.

Como esse paradigma pode ser usado para criar software previsível que funcione conforme o esperado, quando não temos garantia de quando e onde uma expressão será avaliada?


10
Na maioria dos casos, isso simplesmente não importa. Para todos os outros, você pode apenas reforçar o rigor.
Cat Plus Plus

22
O ponto de linguagens puramente funcionais como o haskell é que você não precisa se preocupar quando o código é executado, pois é livre de efeitos colaterais.
bitmask

21
Você precisa parar de pensar em "executar código" e começar a pensar em "calcular resultados", pois é isso que você realmente deseja nos problemas mais interessantes. É claro que os programas geralmente também precisam interagir com o ambiente de alguma forma, mas isso geralmente pode ser reduzido a uma pequena parte do código. De resto, você pode trabalhar puramente funcional , e a preguiça pode tornar o raciocínio muito mais simples.
usar o seguinte código

6
A pergunta no título ("Por que usar a avaliação lenta?") É muito diferente da pergunta no corpo ("Como você usa a avaliação lenta?"). Para o primeiro, veja minha resposta a esta pergunta relacionada .
Daniel Wagner

11
Um exemplo em que a preguiça é útil: no Haskell head . sorttem O(n)complexidade devido à preguiça (não O(n log n)). Consulte Avaliação preguiçosa e complexidade do tempo .
Petr Pudlák 8/09/12

Respostas:


62

Muitas respostas estão relacionadas a listas infinitas e ganhos de desempenho em partes não avaliadas da computação, mas isso está perdendo a maior motivação para a preguiça: a modularidade .

O argumento clássico é apresentado no citado artigo "Por que a programação funcional é importante" (link em PDF), de John Hughes. O exemplo principal desse artigo (Seção 5) é jogar Tic-Tac-Toe usando o algoritmo de busca alfa-beta. O ponto principal é (p. 9):

[Avaliação preguiçosa] torna prático modularizar um programa como um gerador que constrói um grande número de respostas possíveis e um seletor que escolhe o apropriado.

O programa Tic-Tac-Toe pode ser escrito como uma função que gera toda a árvore do jogo iniciando em uma determinada posição e como uma função separada que a consome. No tempo de execução, isso não gera intrinsecamente toda a árvore do jogo, apenas as subpartes de que o consumidor realmente precisa. Podemos mudar a ordem e a combinação na qual as alternativas são produzidas mudando o consumidor; não é necessário trocar o gerador.

Em um idioma ansioso, você não pode escrever dessa maneira, porque provavelmente gastaria muito tempo e memória gerando a árvore. Então você acaba:

  1. Combinando geração e consumo na mesma função;
  2. Escrever um produtor que funcione de maneira ideal apenas para determinados consumidores;
  3. Implementando sua própria versão da preguiça.

Por favor, mais informações ou um exemplo. Isso parece intrigante.
Alex Nye

11
@AlexNye: O jornal John Hughes tem mais informações. Apesar de ser um artigo acadêmico - e, portanto, sem dúvida, intimidador -, na verdade é muito acessível e legível. Se não fosse pelo tamanho, provavelmente serviria como resposta aqui!
Tikhon Jelvis 26/11/12

Talvez para entender essa resposta, é preciso ler o artigo de Hughes ... não tendo feito isso, ainda estou deixando de ver como e por que a preguiça e a modularidade estão relacionadas.
stakx

@stakx Sem uma descrição melhor, eles não parecem estar relacionados, exceto por acaso. A vantagem da preguiça neste exemplo é que um gerador preguiçoso é capaz de gerar todos os estados possíveis do jogo, mas não perde tempo / memória fazendo isso porque apenas os que acontecerem serão consumidos. O gerador pode ser separado do consumidor sem ser um gerador preguiçoso, e é possível (embora mais difícil) ser preguiçoso sem ser separado do consumidor.
Izkata

@ Iztaka: "O gerador pode ser separado do consumidor sem ser um gerador preguiçoso, e é possível (embora mais difícil) ser preguiçoso sem ser separado do consumidor". Observe que, quando você segue essa rota, pode acabar com um ** gerador excessivamente especializado ** - o gerador foi escrito para otimizar um consumidor e, quando reutilizado para outros, é subótimo. Um exemplo comum são os mapeadores objeto-relacionais que buscam e constroem um gráfico de objeto inteiro apenas porque você deseja uma sequência do objeto raiz. A preguiça evita muitos desses casos (mas não todos).
sacundim

32

Como esse paradigma pode ser usado para criar software previsível que funcione conforme o esperado, quando não temos garantia de quando e onde uma expressão será avaliada?

Quando uma expressão é livre de efeitos colaterais, a ordem na qual as expressões são avaliadas não afeta seu valor, portanto, o comportamento do programa não é afetado pela ordem. Portanto, o comportamento é perfeitamente previsível.

Agora os efeitos colaterais são uma questão diferente. Se os efeitos colaterais pudessem ocorrer em qualquer ordem, o comportamento do programa seria realmente imprevisível. Mas esse não é realmente o caso. Idiomas preguiçosos como Haskell fazem questão de ser referencialmente transparente, ou seja, garantir que a ordem na qual as expressões sejam avaliadas nunca afetem seus resultados. Em Haskell, isso é alcançado forçando todas as operações com efeitos colaterais visíveis ao usuário a ocorrer dentro da mônada de E / S. Isso garante que todos os efeitos colaterais ocorram exatamente na ordem que você esperaria.


15
É por isso que apenas idiomas com "pureza forçada", como Haskell, suportam a preguiça em todos os lugares por padrão. Linguagens de "pureza incentivada" como Scala precisam que o programador diga explicitamente onde deseja a preguiça, e cabe ao programador garantir que a preguiça não cause problemas. Uma linguagem que tivesse preguiça por padrão e tivesse efeitos colaterais não rastreados seria realmente difícil de programar previsivelmente.
Ben

11
certamente mônadas diferentes de IO também podem causar efeitos colaterais
jk.

11
@jk Somente IO pode causar efeitos colaterais externos .
precisa saber é o seguinte

@ dave4420 sim, mas não o que esta resposta diz
jk.

11
@jk Em Haskell, na verdade não. Nenhuma mônada, exceto a E / S (ou as que são construídas com E / S), tem efeitos colaterais. E isso ocorre apenas porque o compilador trata as E / S de maneira diferente. Ele considera o IO como "Imutável". Mônadas são apenas uma maneira (inteligente) de garantir uma ordem de execução específica (para que seu arquivo seja excluído somente depois que o usuário digitar "yes").
scarfridge

22

Se você conhece os bancos de dados, uma maneira muito frequente de processar dados é:

  • Faça uma pergunta como select * from foobar
  • Enquanto houver mais dados, faça: obtenha a próxima linha de resultados e processe-a

Você não controla como o resultado é gerado e de que maneira (índices? Varreduras de tabelas completas?) Ou quando (todos os dados devem ser gerados de uma só vez ou incrementalmente quando solicitados?). Tudo o que você sabe é: se houver mais dados, você os obterá quando solicitar.

A avaliação preguiçosa é bem próxima da mesma coisa. Digamos que você tenha uma lista infinita definida como ie. a sequência de Fibonacci - se você precisar de cinco números, obtém cinco números calculados; se você precisar de 1000, recebe 1000. O truque é que o tempo de execução sabe o que fornecer onde e quando. É muito, muito útil.

(Programadores Java podem emular esse comportamento com Iteradores - outras linguagens podem ter algo semelhante)


Bom ponto. Por exemplo Collection2.filter()(assim como os outros métodos dessa classe) implementa uma avaliação lenta: o resultado "parece" um comum Collection, mas a ordem de execução pode ser não intuitiva (ou pelo menos não óbvia). Além disso, existe yieldno Python (e um recurso semelhante no C #, do qual não me lembro o nome), que é ainda mais próximo de dar suporte à avaliação lenta do que um iterador normal.
Joachim Sauer

@JoachimSauer em C # seu retorno de rendimento, ou, é claro, você pode usar os opreradores linq de pré-construção, cerca da metade dos quais são preguiçosos
jk.

+1: Para mencionar iteradores em uma linguagem imperativa / orientada a objetos. Eu usei uma solução semelhante para implementar fluxos e funções de fluxo em Java. Usando iteradores, eu poderia ter funções como take (n), dropWhile () em um fluxo de entrada de comprimento desconhecido.
Giorgio

13

Considere pedir ao seu banco de dados uma lista dos primeiros 2.000 usuários cujos nomes começam com "Ab" e têm mais de 20 anos. Eles também devem ser do sexo masculino.

Aqui está um pequeno diagrama.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

Como você pode ver por essa terrível interação terrível, o "banco de dados" não está fazendo nada até estar pronto para lidar com todas as condições. É o carregamento lento dos resultados em cada etapa e a aplicação de novas condições a cada vez.

Ao contrário de obter os primeiros 2.000 usuários, devolvê-los, filtrá-los para "Ab", retorná-los, filtrá-los por mais de 20, devolvê-los e filtrar por homens e finalmente devolvê-los.

Carregamento preguiçoso em poucas palavras.


11
esta é uma explicação realmente ruim IMHO. Infelizmente, eu não tenho representante suficiente neste site SE em particular para votar. O ponto real da avaliação preguiçosa é que nenhum desses resultados é realmente produzido até que algo mais esteja pronto para consumi-los.
Alnitak

Minha resposta postada está dizendo exatamente a mesma coisa que seu comentário.
sergserg

Esse é um processador de programa muito educado.
Julian

9

A avaliação lenta das expressões fará com que o designer de um determinado pedaço de código perca o controle sobre a sequência em que o código é executado.

O designer não deve se preocupar com a ordem em que as expressões são avaliadas, desde que o resultado seja o mesmo. Adiando a avaliação, pode ser possível evitar a avaliação completa de algumas expressões, o que economiza tempo.

Você pode ver a mesma ideia em ação em um nível inferior: muitos microprocessadores são capazes de executar instruções fora de ordem, o que lhes permite usar suas várias unidades de execução com mais eficiência. A chave é que eles examinem as dependências entre as instruções e evitem reordenar onde isso mudaria o resultado.


5

Existem vários argumentos para avaliação preguiçosa que eu acho convincentes

  1. Modularidade Com uma avaliação lenta, você pode dividir o código em partes. Por exemplo, suponha que você tenha o problema de "localizar os dez primeiros recíprocos de elementos em uma lista de lista, de modo que os recíprocos sejam menores que 1." Em algo como Haskell, você pode escrever

    take 10 . filter (<1) . map (1/)
    

    mas isso está incorreto em um idioma estrito, pois se você der, [2,3,4,5,6,7,8,9,10,11,12,0]estará dividindo por zero. Veja a resposta de Sacundim para saber por que isso é incrível na prática

  2. Mais coisas funcionam Estritamente (trocadilhos) mais programas terminam com uma avaliação não estrita do que com uma avaliação estrita. Se o seu programa terminar com uma estratégia de avaliação "ansiosa", será finalizada com uma estratégia "preguiçosa", mas o contrário não será verdadeiro. Você obtém coisas como estruturas de dados infinitas (que são realmente muito legais) como exemplos específicos desse fenômeno. Mais programas funcionam em idiomas preguiçosos.

  3. Ótima avaliação A avaliação por necessidade é assintoticamente ideal em relação ao tempo. Embora as principais linguagens preguiçosas (que são essencialmente Haskell e Haskell) não prometam atender às necessidades, você pode mais ou menos esperar um modelo de custo ideal. Analisadores de rigidez (e avaliação especulativa) mantêm a sobrecarga na prática. O espaço é uma questão mais complicada.

  4. A pureza das forças usando a avaliação preguiçosa faz com que lidar com os efeitos colaterais de maneira indisciplinada seja uma dor total, porque, como você diz, o programador perde o controle. Isto é uma coisa boa. A transparência referencial facilita muito a programação, a refatoração e o raciocínio sobre os programas. Linguagens estritas inevitavelmente cedem à pressão de ter bits impuros - algo que Haskell e Clean resistiram lindamente. Isso não quer dizer que os efeitos colaterais sejam sempre ruins, mas controlá-los é tão útil que apenas esse motivo é suficiente para usar linguagens preguiçosas.


2

Suponha que você tenha muitos cálculos caros em oferta, mas não sabe quais serão realmente necessários ou em que ordem. Você pode adicionar um protocolo complicado de mãe para mãe para forçar o consumidor a descobrir o que está disponível e acionar cálculos que ainda não foram feitos. Ou você pode apenas fornecer uma interface que atue como se todos os cálculos tivessem sido feitos.

Além disso, suponha que você tenha um resultado infinito. O conjunto de todos os números primos, por exemplo. É óbvio que você não pode calcular o conjunto antecipadamente, portanto, qualquer operação no domínio de números primos deve ser preguiçosa.


1

com uma avaliação lenta, você não perde o controle sobre a execução do código, ainda é absolutamente determinístico. É difícil se acostumar com isso, no entanto.

avaliação preguiçosa é útil porque é uma forma de redução do prazo lambda que terminará em alguns casos, onde a avaliação ansiosa falhará, mas não vice-versa. Isso inclui 1) quando você precisa vincular ao resultado da computação antes de executar a computação, por exemplo, quando constrói a estrutura gráfica cíclica, mas deseja fazê-lo no estilo funcional 2) quando define uma estrutura de dados infinita, mas funciona com esse feed de estrutura para usar apenas parte da estrutura de dados.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.