Pergunta

Há muito tempo se perguntando por avaliação preguiçosa é útil. Eu ainda tenho que ter alguém explicar-me de uma maneira que faz sentido; principalmente acaba de ebulição baixo para "Trust Me".

Nota: eu faço memoization não média.

Foi útil?

Solução

Principalmente porque pode ser mais eficiente - valores não precisam ser computado se eles não estão indo para ser usado. Por exemplo, eu posso passar três valores para uma função, mas, dependendo da sequência de expressões condicionais, pode realmente ser utilizado apenas um subconjunto. Em uma linguagem como C, todos os três valores seriam computados de qualquer maneira; mas em Haskell, apenas os valores necessários são computados.

Ele também permite coisas legais como as listas infinitas. Eu não posso ter uma lista infinita em uma linguagem como C, mas em Haskell, isso não é problema. listas infinitas são utilizados com bastante frequência em certas áreas da matemática, por isso pode ser útil ter a capacidade de manipulá-los.

Outras dicas

Um exemplo útil de avaliação lenta é a utilização de quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Se agora quer encontrar o mínimo da lista, podemos definir

minimum ls = head (quickSort ls)

Qual primeira classifica a lista e, em seguida, toma o primeiro elemento da lista. No entanto, por causa da avaliação preguiçosa, apenas a cabeça fica computado. Por exemplo, se tomarmos o mínimo da lista [2, 1, 3,] QuickSort vai primeiro filtrar todos os elementos que são menores do que dois. Em seguida, ele faz QuickSort em que (retornando a lista Singleton [1]), que já é suficiente. Devido a avaliação preguiçosa, o resto não é ordenada, economizando um monte de tempo computacional.

Este é, naturalmente, um exemplo muito simples, mas obras preguiça da mesma forma para programas que são muito grandes.

Há, no entanto, uma desvantagem para tudo isso: torna-se mais difícil prever a velocidade de execução e uso de memória do seu programa. Isso não significa que os programas de preguiçosos são mais lentos ou tomar mais memória, mas é bom saber.

Eu acho avaliação preguiçosa útil para uma série de coisas.

Em primeiro lugar, todas as línguas preguiçosos existentes são puros, porque é muito difícil raciocinar sobre efeitos colaterais em uma linguagem de preguiçoso.

línguas Pure deixá-lo raciocinar sobre definições de funções usando raciocínio equational.

foo x = x + 3

Infelizmente em um ambiente não-preguiçoso, mais declarações não devolver do que em um ambiente preguiçoso, por isso este é menos útil em linguagens como ML. Mas em uma língua preguiçoso você pode seguramente razão sobre a igualdade.

Em segundo lugar, um monte de coisas como o 'restrição valor' em ML não são necessários em linguagens preguiçosas como Haskell. Isto leva a uma grande decluttering da sintaxe. ML como línguas precisa usar palavras-chave como var ou diversão. Em Haskell estas coisas em colapso para baixo a uma noção.

Em terceiro lugar, a preguiça permite escrever código muito funcional que pode ser entendida em pedaços. Em Haskell é comum para escrever um corpo da função como:

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Isso permite trabalhar 'top down' embora o entendimento do corpo de uma função. ML-como línguas forçá-lo a usar um let que é avaliada rigorosamente. Consequentemente, você não ousa 'levantar' a cláusula let para o corpo principal da função, porque se caro (ou tem efeitos colaterais) que você não quer que ele sempre a ser avaliada. Haskell pode 'empurrar' os detalhes para a cláusula WHERE explicitamente porque sabe que o conteúdo dessa cláusula só serão avaliados conforme necessário.

Na prática, temos a tendência de guardas de uso e colapso que, para além:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Em quarto lugar, por vezes, a preguiça ofertas muito mais elegante expressão de determinados algoritmos. Um preguiçoso 'quick tipo' em Haskell é um one-liner e tem a vantagem de que, se você só olhar para os primeiros itens, você só pagar os custos proporcional ao custo de selecionar esses itens. Nada o impede de fazer isso de forma rigorosa, mas você provavelmente terá que recodificar o algoritmo de cada vez para alcançar o mesmo desempenho assintótica.

Em quinto lugar, a preguiça permite definir novas estruturas de controle no idioma. Você não pode escrever um novo 'se .. então .. mais ..' como construção em uma linguagem rigorosa. Se você tentar definir uma função como:

if' True x y = x
if' False x y = y

em uma linguagem rigorosa, em seguida, ambos os ramos seria avaliado independentemente do valor da condição. Fica pior quando você considera loops. Todas as soluções rigorosas exigem a língua para fornecê-lo com algum tipo de citação ou construção lambda explícito.

Finalmente, nesse mesmo sentido, alguns dos melhores mecanismos para lidar com os efeitos colaterais no sistema de tipo, como mônadas, realmente só pode ser expresso de forma eficaz em um ambiente preguiçoso. Isso pode ser testemunhado pela comparação da complexidade dos fluxos de trabalho de F # para Haskell Mônadas. (Você pode definir uma mônada em uma linguagem rigorosa, mas, infelizmente, muitas vezes você vai deixar uma lei mônada ou dois devido à falta de preguiça e fluxos de trabalho por comparação pegar uma tonelada de bagagem rigoroso.)

Há uma diferença entre a avaliação ordem normal uma avaliação preguiçosa (como em Haskell).

square x = x * x

Avaliando a seguinte expressão ...

square (square (square 2))

... com avaliação ansiosa:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... com avaliação normal de ordem:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... com avaliação preguiçosa:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

Isso porque aparência avaliação preguiçosa na árvore de sintaxe e faz árvores transformações ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... enquanto a avaliação normal de fim só faz expansões textuais.

É por isso que, quando se utiliza avaliação preguiçosa, receber mais poderosa (termina de avaliação mais frequentemente, em seguida, outras estratégias), enquanto o desempenho é equivalente a avaliação ansiosa (pelo menos em O-notação).

Avaliação preguiçoso relacionadas com CPU da mesma forma que a coleta de lixo relacionado a RAM. GC permite que você fingir que você tem uma quantidade ilimitada de memória e, assim, solicitar como muitos objetos na memória como você precisa. Runtime irá recuperar automaticamente objetos inutilizáveis. LE permite que você fingir que você tem recursos computacionais ilimitado - você pode fazer como muitos cálculos que você precisar. Runtime só não vai executar desnecessário (para determinado caso) cálculos.

Qual é a vantagem prática desses modelos "fingindo"? Ela libera desenvolvedor (até certo ponto) a partir de recursos de gestão e remove algum código clichê de suas fontes. Mas o mais importante é que você pode eficientemente reutilizar sua solução em conjunto mais amplo de contextos.

Imagine que você tem uma lista de números S e um número N. Você precisa encontrar o mais próximo de número N número M da lista S. Você pode ter dois contextos: single N e alguma lista L de Ns (ei para cada N em L você olhar para cima o M mais próximo em S). Se você usar a avaliação lenta, você pode classificar S e aplicar a pesquisa binária para encontrar mais próximo M para N. Para um bom preguiçoso classificação exigirá O passos para única N e O (ln (tamanho ((S)) (S)) * (tamanho (S) + tamanho (G))) passos para igualmente distribuído L. Se não têm avaliação lenta para atingir a eficiência óptima tem de implementar algoritmo para cada contexto.

Se você acredita Simon Peyton Jones, avaliação preguiçosa não é importante per se , mas apenas como uma 'camisa de cabelo', que forçou os designers para manter a língua pura. Encontro-me solidário com este ponto de vista.

Richard Bird, John Hughes, e em menor extensão Ralf Hinze são capazes de fazer coisas incríveis com avaliação preguiçosa. Lendo o seu trabalho vai ajudá-lo a apreciá-lo. Para bons pontos de partida são magníficas Sudoku solver Bird href="http://www.haskell.org/haskellwiki/Sudoku#Sudoku_incrementally.2C_.C3.A0_la_Bird" e papel de Hughes em A importância da programação funcional .

Considere um programa tic-tac-toe. Este tem quatro funções:

  • A função move-geração que leva uma placa atual e gera uma lista de novas placas cada um com um movimento aplicada.
  • Depois, há uma função "move árvore" que se aplica a função de movimento geração para derivar todas as posições possíveis do conselho que poderia seguir de um presente.
  • Existe uma função minimax que anda a árvore (ou possivelmente apenas parte dele) para encontrar o melhor movimento seguinte.
  • Existe uma função placa-avaliação que determina se um dos jogadores ganhou.

Isso cria uma separação clara agradável de preocupações. Em particular a função de mover-geração e as funções de avaliação bordo são os únicos que precisam entender as regras do jogo:. A árvore de movimento e funções minimax são completamente reutilizáveis ??

Agora vamos tentar implementar xadrez em vez de tic-tac-toe. Em uma linguagem "ansioso" (ou seja, convencional) isso não vai funcionar porque a árvore movimento não vai caber na memória. Então agora as funções de geração de avaliação bordo e movimento precisa ser misturado com a árvore de movimento e lógica minimax porque a lógica minimax tem que ser usado para decidir quais movimentos de gerar. Nossa estrutura modular limpa agradável desaparece.

No entanto, em uma linguagem preguiçosa os elementos da árvore de movimento são gerados apenas em resposta às exigências da função minimax: toda a árvore de movimento não precisa ser gerado antes de deixar minimax solta no elemento superior. Portanto, a nossa estrutura modular limpa ainda funciona em um jogo real.

Aqui estão mais dois pontos que eu não acredito que foram levantadas na discussão ainda.

  1. A preguiça é um mecanismo de sincronização em um ambiente concorrente. É uma maneira leve e fácil de criar uma referência a alguma computação, e compartilhar seus resultados entre muitos tópicos. Se vários segmentos tentam acessar um valor não avaliada, apenas um deles irá executá-lo, e os outros vão bloquear consequentemente, receber o valor, uma vez que se torna disponível.

  2. A preguiça é fundamental para amortizar estruturas de dados em um ambiente puro. Este é descrito por Okasaki em Estruturas de Dados puramente funcional em detalhes, mas a idéia básica é que a avaliação preguiçoso é uma forma controlada de mutação fundamental para nos permitir implementar certos tipos de estruturas de dados de forma eficiente. Enquanto muitas vezes falamos de preguiça forçando-nos a usar o hairshirt pureza, a outra maneira se aplica também:. Eles são um par de recursos de linguagem sinérgicos

Quando você liga o computador eo Windows abstém-se de abrir cada diretório único no seu disco rígido no Windows Explorer e abstém-se de lançar qualquer programa único instalado no seu computador, até que indicam que um determinado diretório é necessário ou um determinado programa é necessário, que é a avaliação "preguiçoso".

avaliação "Lazy" é a realização de operações quando e como eles são necessários. É útil quando se é uma característica de uma linguagem de programação ou biblioteca, porque geralmente é mais difícil de implementar avaliação preguiçosa em seu próprio país do que simplesmente a tudo precalculate na frente.

  1. Pode aumentar a eficiência. Esta é a mais óbvia para o futuro, mas não é realmente o mais importante. (Note-se também que a preguiça pode kill eficiência também - este fato não é imediatamente óbvio No entanto, armazenando-se muitos resultados temporários em vez de calculá-los imediatamente, você pode usar-se uma enorme quantidade de RAM.).

  2. Ele permite que você defina o fluxo construções de controle em código em nível de usuário normal, em vez de ser codificado para a língua. (Por exemplo, Java tem laços for; Haskell tem uma função for Java tem tratamento de exceção;. Haskell tem vários tipos de mônada exceção C # tem goto;. Haskell tem a Mônada continuação ...)

  3. Ele permite que você dissociar o algoritmo para gerando dados do algoritmo para decidir quanto dados para gerar. Você pode escrever uma função que gera uma lista teoricamente infinita de resultados, e outra função que processos como grande parte desta lista, uma vez que decide que ele precisa. Mais ao ponto, você pode ter de cinco funções de gerador e de cinco funções de consumo, e você pode eficientemente produzir qualquer combinação - em vez de codificação manualmente 5 x 5 = 25 funções que combinam ambas as ações ao mesmo tempo. (!) Nós todos dissociação saber é uma coisa boa.

  4. É mais ou menos força você a projetar um pura linguagem funcional. É sempre tentador tomar atalhos, mas em uma língua preguiçoso, a menor impureza torna seu código descontroladamente imprevisível, que fortemente milita contra a tomar atalhos.

Considere o seguinte:

if (conditionOne && conditionTwo) {
  doSomething();
}

O método doSomething () será executado somente se conditionOne é verdade e conditionTwo é verdade. No caso em que conditionOne é falso, por que você precisa para calcular o resultado da conditionTwo? A avaliação da conditionTwo será um desperdício de tempo, neste caso, especialmente se sua condição é o resultado de algum processo de método.

Esse é um exemplo do interesse avaliação preguiçosa ...

Um grande benefício da preguiça é a capacidade de escrever estruturas de dados imutáveis ??com limites amortizados razoáveis. Um exemplo simples é uma pilha imutável (usando F #):

type 'a stack =
    | EmptyStack
    | StackNode of 'a * 'a stack

let rec append x y =
    match x with
    | EmptyStack -> y
    | StackNode(hd, tl) -> StackNode(hd, append tl y)

O código é razoável, mas anexando duas pilhas x e y leva O (comprimento x) vez em melhores, piores, e os casos médios. Anexando duas pilhas é uma operação monolítico, que toca todos os nós na pilha x.

Podemos re-escrever a estrutura de dados como uma pilha preguiçoso:

type 'a lazyStack =
    | StackNode of Lazy<'a * 'a lazyStack>
    | EmptyStack

let rec append x y =
    match x with
    | StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
    | Empty -> y

lazy funciona, suspendendo a avaliação de código em seu construtor. Uma vez avaliada usando .Force(), o valor de retorno é armazenado em cache e reutilizada em cada .Force() posterior.

Com a versão preguiçoso, Anexa é um O (1) operação: ele retorna 1 nó e suspende a reconstrução real da lista. Quando você chegar à frente desta lista, ele irá avaliar o conteúdo do nó, forçando-o retornar a cabeça e criar uma suspensão com os restantes elementos, de modo a tomar a cabeça da lista é um O (1) operação.

Assim, a nossa lista preguiçoso está em um estado constante de reconstrução, você não paga o custo para reconstruir esta lista até que você atravessar todos os seus elementos. Usando preguiça, esta lista suportes O (1) consing e anexando. Curiosamente, desde que não avaliam nós até sua acessado, sua subsidiária possível construir uma lista com elementos potencialmente infinitas.

A estrutura de dados acima não requer nodos para ser recalculados em cada travessia, de modo que são distintamente diferentes dos IEnumerables baunilha em .NET.

avaliação

preguiçoso é mais útil com estruturas de dados. Você pode definir uma matriz ou vector indutivamente especificando apenas certos pontos da estrutura e expressar todos os outros em termos de todo o conjunto. Isto permite gerar estruturas de dados muito concisa e com alto desempenho em tempo de execução.

Para ver isso em ação, você pode ter um olhar para a minha biblioteca de rede neural chamado instinto . Ele faz uso pesado de avaliação preguiçosa para a elegância e alto desempenho. Por exemplo, eu totalmente livrar-se do cálculo de ativação tradicionalmente imperativo. A expressão preguiçoso simples faz tudo para mim.

Isto é usado por exemplo no função de ativação e também no algoritmo de aprendizagem backpropagation (só posso postar dois links, então você precisa de olhar para cima a função learnPat no módulo AI.Instinct.Train.Delta você mesmo). Tradicionalmente ambos exigem muito mais complicado algoritmos iterativos.

Este trecho mostra a diferença entre avaliação preguiçosa preguiçoso e não. É claro que esta função de Fibonacci em si poderia ser otimizado e usar avaliação preguiçosa em vez de recursão, mas que iria estragar o exemplo.

Vamos supor que pode tem que usar os 20 primeiros números para algo, não com avaliação preguiçosa todos os 20 números tem que ser gerada antecipadamente, mas, com avaliação preguiçosa que vai ser gerado, conforme necessário só. Assim, você vai pagar apenas o preço de cálculo quando necessário.

Exemplo de saída

Not lazy generation: 0.023373
Lazy generation: 0.000009
Not lazy output: 0.000921
Lazy output: 0.024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)

Outras pessoas já deu todas as grandes razões, mas acho que um exercício útil para ajudar a compreender as questões por que a preguiça é tentar escrever uma função de ponto-fixo em uma linguagem rigorosa.

Em Haskell, uma função de ponto fixo é super fácil:

fix f = f (fix f)

Isso expande a

f (f (f ....

mas porque Haskell é preguiçoso, que cadeia infinita de computação não é problema; a avaliação é feita "fora-de-dentro", e tudo funciona maravilhosamente:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

É importante, não importa que fix ser preguiçoso, mas que f ser preguiçoso. Uma vez que você já tenha sido dada uma f rigoroso, você pode jogar suas mãos no ar e desistir, ou eta expandi-lo e desorganização coisas. (Isto é muito parecido com o que Noah estava dizendo sobre ele ser o biblioteca que é estrita / preguiçoso, não a língua).

Agora imagine escrever a mesma função no Scala rigoroso:

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

Você de ficar claro que um estouro de pilha. Se você quer que ele funcione, você precisa para fazer a chamada por necessidade argumento f:

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)

Eu não sei como você atualmente pensar em coisas, mas acho que é útil pensar avaliação preguiçosa como uma questão de biblioteca em vez de um recurso de linguagem.

Quer dizer que em linguagens rigorosas, posso implementar a avaliação lenta através da construção de algumas estruturas de dados, e em línguas preguiçosos (pelo menos Haskell), eu posso pedir rigor quando eu quiser. Portanto, a opção de idioma não faz realmente seus programas preguiçoso ou não preguiçoso, mas simplesmente afeta o que você recebe por padrão.

Uma vez que você pensar sobre isso assim, então pensar em todos os lugares onde você escrever uma estrutura de dados que mais tarde você pode usar para gerar dados (sem olhar para ele muito antes disso), e você vai ver um monte de usos para avaliação preguiçosa.

A exploração mais útil de avaliação preguiçosa que eu usei foi uma função que chamou uma série de sub-funções em uma determinada ordem. Se qualquer um destes sub-funções falhou (falsos voltou), a função de chamar precisava voltar imediatamente. Então, eu poderia ter feito isso desta maneira:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

ou, a solução mais elegante:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Uma vez que você começar a usá-lo, você verá oportunidades para usá-lo mais e mais vezes.

Sem avaliação preguiçosa não será permitido escrever algo como isto:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

Entre outras coisas, linguagens preguiçosas permitir estruturas de dados infinita multidimensionais.

Enquanto esquema, python, etc permitir estruturas de dados infinitas dimensões individuais com riachos, você só pode transversal ao longo de uma dimensão.

A preguiça é útil para a mesma franja problema , mas vale a pena notar a conexão coroutines mencionado nesse link.

Avaliação preguiçoso é um raciocínio equational pobre homem (o que poderia ser esperado, idealmente, ser deduzir propriedades do código de propriedades de tipos e operações envolvidas).

Exemplo onde ele funciona muito bem: sum . take 10 $ [1..10000000000]. Que não se importa de ser reduzido a uma soma de 10 números, em vez de apenas um cálculo numérico direto e simples. Sem a avaliação preguiçosa é claro que isso iria criar uma lista gigantesca na memória apenas para usar seus primeiros 10 elementos. Seria certamente muito lento, e pode causar um erro de falta de memória.

Exemplo onde não é tão grande como gostaríamos: sum . take 1000000 . drop 500 $ cycle [1..20]. Que realmente vai somar os números 1 000 000, mesmo em um loop em vez de em uma lista; ainda que deve ser reduzido a apenas um cálculo numérico direta, com poucas condicionais e algumas fórmulas. Que se ser muito melhor, em seguida, somando os números 1 000 000. Mesmo em um loop, e não em uma lista (ou seja, após a otimização desmatamento).


Outra coisa é, torna-se possível o código em cauda recursão modulo contras estilo, e simplesmente funciona .

cf. relacionada resposta .

Se por "avaliação preguiçosa" você quer dizer como em booleans combound, como em

   if (ConditionA && ConditionB) ... 

, em seguida, a resposta é simplesmente que quanto menos ciclos de CPU as consome programa, mais rápido ele vai correr ... e se um pedaço de processamento instruções não terá impacto sobre o resultado do programa, então é desnecessário, (e portanto, um desperdício de tempo) para realizá-las de qualquer maneira ...

Se otoh, você quer dizer o que tenho conhecido como "initializers preguiçosos", como em:

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

Bem, esta técnica permite que o código do cliente usando a classe para evitar a necessidade de chamar o banco de dados para o registro de dados Supervisor exceto quando o cliente usando o objeto Employee requer acesso aos dados do supervisor ... isso torna o processo de instanciar um empregado mais rápido, e ainda quando você precisa do Supervisor, a primeira chamada para a propriedade Supervisor irá acionar a chamada de banco de dados e os dados será obtida e disponível ...

Superior

Vamos encontrar o maior número em 100.000 que é divisível por 3829. Para fazer isso, vamos filtrar um conjunto de possibilidades em que sabemos as mentiras da solução.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Em primeiro lugar, fazer uma lista de todos os números mais baixos do que 100.000, descendente. Em seguida, filtrá-lo pela nossa predicado e porque os números são classificadas de forma decrescente, o maior número que satisfaz nossos predicado é o primeiro elemento da lista filtrada. Nós nem sequer precisa usar uma lista finita para o nosso conjunto inicial. Isso é preguiça em ação novamente. Porque nós só acabam usando a cabeça do filtrado lista, não importa se a lista filtrada é finito ou infinito. A avaliação pára quando a primeira solução adequada é encontrado.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top