Há muito tempo me pergunto por que a avaliação preguiçosa é útil. Ainda não tenho ninguém que me explique de uma forma que faça sentido; na maioria das vezes, acaba se resumindo a "confiar em mim".
Nota: não quero dizer memoização.
Há muito tempo me pergunto por que a avaliação preguiçosa é útil. Ainda não tenho ninguém que me explique de uma forma que faça sentido; na maioria das vezes, acaba se resumindo a "confiar em mim".
Nota: não quero dizer memoização.
Respostas:
Principalmente porque pode ser mais eficiente - os valores não precisam ser calculados se não forem usados. Por exemplo, posso passar três valores para uma função, mas dependendo da sequência de expressões condicionais, apenas um subconjunto pode realmente ser usado. Em uma linguagem como C, todos os três valores seriam calculados de qualquer maneira; mas em Haskell, apenas os valores necessários são calculados.
Também permite coisas legais como listas infinitas. Não posso ter uma lista infinita em uma linguagem como C, mas em Haskell, isso não é problema. Listas infinitas são usadas com bastante frequência em certas áreas da matemática, portanto, pode ser útil ter a habilidade de manipulá-las.
Um exemplo útil de avaliação preguiçosa é o uso de quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
Se agora queremos encontrar o mínimo da lista, podemos definir
minimum ls = head (quickSort ls)
Que primeiro classifica a lista e, em seguida, obtém o primeiro elemento da lista. No entanto, por causa da avaliação preguiçosa, apenas a cabeça é computada. Por exemplo, se pegarmos o mínimo da lista, [2, 1, 3,]
quickSort filtrará primeiro todos os elementos menores que dois. Em seguida, ele faz quickSort sobre isso (retornando a lista de singleton [1]) que já é suficiente. Por causa da avaliação preguiçosa, o resto nunca é classificado, economizando muito tempo computacional.
É claro que este é um exemplo muito simples, mas a preguiça funciona da mesma maneira para programas muito grandes.
No entanto, há uma desvantagem em tudo isso: fica mais difícil prever a velocidade do tempo de execução e o uso de memória de seu programa. Isso não significa que programas preguiçosos sejam mais lentos ou ocupem mais memória, mas é bom saber.
take k $ quicksort list
leva apenas um tempo O (n + k log k), onde n = length list
. Com uma classificação de comparação não preguiçosa, isso sempre levaria O (n log n) tempo.
Considero a avaliação preguiçosa útil para várias coisas.
Em primeiro lugar, todas as linguagens preguiçosas existentes são puras, porque é muito difícil raciocinar sobre os efeitos colaterais em uma linguagem preguiçosa.
Linguagens puras permitem raciocinar sobre definições de funções usando raciocínio equacional.
foo x = x + 3
Infelizmente, em uma configuração não preguiçosa, mais instruções falham ao retornar do que em uma configuração preguiçosa, portanto, isso é menos útil em linguagens como ML. Mas em uma linguagem preguiçosa você pode raciocinar com segurança sobre igualdade.
Em segundo lugar, muitas coisas como a 'restrição de valor' em ML não são necessárias em linguagens preguiçosas como Haskell. Isso leva a uma grande organização de sintaxe. Linguagens como ML precisam usar palavras-chave como var ou fun. Em Haskell, essas coisas se resumem a uma única noção.
Terceiro, a preguiça permite que você escreva um código muito funcional que pode ser entendido em partes. Em Haskell, é comum escrever um corpo de 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 que você trabalhe 'de cima para baixo' por meio da compreensão do corpo de uma função. As linguagens do tipo ML forçam você a usar um let
que é avaliado de forma estrita. Consequentemente, você não ousa 'elevar' a cláusula let para o corpo principal da função, porque se ela for cara (ou tiver efeitos colaterais), você não quer que ela seja sempre avaliada. Haskell pode 'empurrar' os detalhes para a cláusula where explicitamente porque sabe que o conteúdo dessa cláusula só será avaliado conforme necessário.
Na prática, tendemos a usar proteções e reduzi-las para:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Quarto, às vezes a preguiça oferece uma expressão muito mais elegante de certos algoritmos. Uma 'classificação rápida' preguiçosa em Haskell é uma linha única e tem a vantagem de que, se você olhar apenas os primeiros itens, pagará apenas custos proporcionais ao custo de selecionar apenas esses itens. Nada impede que você faça isso estritamente, mas provavelmente você terá que recodificar o algoritmo a cada vez para obter o mesmo desempenho assintótico.
Quinto, a preguiça permite que você defina novas estruturas de controle na linguagem. Você não pode escrever uma nova construção 'if .. then .. else ..' em uma linguagem estrita. Se você tentar definir uma função como:
if' True x y = x
if' False x y = y
em uma linguagem estrita, ambos os ramos seriam avaliados independentemente do valor da condição. Fica pior quando você considera os loops. Todas as soluções restritas requerem que a linguagem forneça algum tipo de citação ou construção lambda explícita.
Finalmente, nessa mesma linha, alguns dos melhores mecanismos para lidar com os efeitos colaterais no sistema de tipos, como as mônadas, só podem ser expressos efetivamente em um ambiente preguiçoso. Isso pode ser testemunhado comparando a complexidade dos fluxos de trabalho do F # com as mônadas de Haskell. (Você pode definir uma mônada em uma linguagem estrita, mas infelizmente você frequentemente falhará em uma ou duas leis de mônada devido à falta de preguiça e os Fluxos de Trabalho, em comparação, pegam uma tonelada de bagagem estrita.)
let
é uma besta perigosa; no esquema R6RS, ele permite que aleatórios #f
apareçam em seu termo onde quer que o nó estritamente leve a um ciclo! Sem trocadilhos, mas let
ligações estritamente mais recursivas são adequadas em uma linguagem preguiçosa. A rigidez também exacerba o fato de quewhere
não há como ordenar os efeitos relativos, exceto pelo SCC, é uma construção no nível da instrução, seus efeitos podem acontecer em qualquer ordem estritamente, e mesmo se você tiver uma linguagem pura, acabará com o #f
questão. Estrita where
enigmas seu código com preocupações não-locais.
ifFunc(True, x, y)
vai avaliar tanto x
e ao y
invés de apenas x
.
Há uma diferença entre a avaliação de ordem normal e a 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 de pedido normal:
> (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 ocorre porque a avaliação preguiçosa olha para a árvore de sintaxe e faz transformações de árvore ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... enquanto a avaliação normal de pedidos só faz expansões textuais.
É por isso que, ao usar a avaliação preguiçosa, ficamos mais poderosos (a avaliação termina com mais frequência do que outras estratégias), enquanto o desempenho é equivalente à avaliação ansiosa (pelo menos em notação O).
A avaliação lenta relacionada à CPU da mesma forma que a coleta de lixo relacionada à RAM. O GC permite que você finja que tem uma quantidade ilimitada de memória e, portanto, solicite quantos objetos na memória forem necessários. O tempo de execução recuperará automaticamente os objetos inutilizáveis. O LE permite que você finja que possui recursos computacionais ilimitados - você pode fazer quantos cálculos precisar. O tempo de execução simplesmente não executará cálculos desnecessários (para determinado caso).
Qual é a vantagem prática desses modelos "fingidos"? Ele libera o desenvolvedor (até certo ponto) do gerenciamento de recursos e remove algum código clichê de suas fontes. Mas o mais importante é que você pode reutilizar com eficiência sua solução em um 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 do número N número M da lista S. Você pode ter dois contextos: único N e alguma lista L de Ns (ei para cada N em L você procura o M mais próximo em S). Se você usar a avaliação preguiçosa, pode classificar S e aplicar a pesquisa binária para encontrar o M mais próximo de N. Para uma boa classificação preguiçosa, serão necessários O (tamanho (S)) etapas para N único e O (ln (tamanho (S)) * (tamanho (S) + tamanho (L))) etapas para L. igualmente distribuído. Se você não tem uma avaliação preguiçosa para atingir a eficiência ideal, você deve implementar o algoritmo para cada contexto.
Se você acredita em Simon Peyton Jones, a avaliação preguiçosa não é importante per se mas apenas como uma 'camisa de cabelo' que forçou os designers a manter a linguagem pura. Acho que simpatizo com esse ponto de vista.
Richard Bird, John Hughes e, em menor grau, Ralf Hinze são capazes de fazer coisas incríveis com avaliação preguiçosa. Ler o trabalho deles o ajudará a apreciá-lo. Um bom ponto de partida é o magnífico solucionador de Sudoku de Bird e o artigo de Hughes sobre Por que a programação funcional é importante .
IO
mônada) a assinatura de main
seria String -> String
e você já pudesse escrever programas interativos apropriadamente.
IO
mônada?
Considere um programa tic-tac-toe. Isso tem quatro funções:
Isso cria uma boa e clara separação de interesses. Em particular, a função de geração de movimento e as funções de avaliação do tabuleiro são as únicas que precisam entender as regras do jogo: a árvore de movimento e as funções do minimax são totalmente reutilizáveis.
Agora vamos tentar implementar o xadrez em vez do jogo da velha. Em uma linguagem "ansiosa" (isto é, convencional), isso não funcionará porque a árvore de movimentação não caberá na memória. Portanto, agora as funções de avaliação do tabuleiro e geração de movimentos precisam ser combinadas com a árvore de movimentos e a lógica do minimax, porque a lógica do minimax deve ser usada para decidir quais movimentos gerar. Nossa estrutura modular limpa e agradável desaparece.
No entanto, em uma linguagem preguiçosa, os elementos da árvore de movimentação são gerados apenas em resposta às demandas da função minimax: a árvore de movimentação inteira não precisa ser gerada antes de deixarmos o minimax solto no elemento superior. Portanto, nossa estrutura modular limpa ainda funciona em um jogo real.
Aqui estão mais dois pontos que, creio, ainda não foram levantados na discussão.
A preguiça é um mecanismo de sincronização em um ambiente concorrente. É uma maneira leve e fácil de criar uma referência para alguns cálculos e compartilhar seus resultados entre muitos threads. Se vários threads tentarem acessar um valor não avaliado, apenas um deles irá executá-lo e os outros irão bloquear de acordo, recebendo o valor assim que estiver disponível.
A preguiça é fundamental para amortizar estruturas de dados em um ambiente puro. Isso é descrito por Okasaki em Purely Functional Data Structures em detalhes, mas a ideia básica é que a avaliação preguiçosa é uma forma controlada de mutação crítica para nos permitir implementar certos tipos de estruturas de dados com eficiência. Embora muitas vezes falemos de preguiça nos forçando a usar o hairshirt puro, o outro caminho também se aplica: eles são um par de características de linguagem sinérgica.
Quando você liga seu computador e o Windows se abstém de abrir todos os diretórios do seu disco rígido no Windows Explorer e evita de iniciar todos os programas instalados no computador, até que você indique que um determinado diretório é necessário ou um determinado programa é necessário, isso é avaliação "preguiçosa".
A avaliação "preguiçosa" está realizando operações quando e conforme são necessárias. É útil quando é um recurso de uma linguagem de programação ou biblioteca porque geralmente é mais difícil implementar uma avaliação preguiçosa por conta própria do que simplesmente calcular tudo antecipadamente.
Considere isto:
if (conditionOne && conditionTwo) {
doSomething();
}
O método doSomething () será executado apenas se conditionOne for verdadeiro e conditionTwo for true. No caso em que conditionOne é falso, por que você precisa calcular o resultado de conditionTwo? A avaliação de conditionTwo será uma perda de tempo neste caso, especialmente se sua condição for o resultado de algum processo de método.
Esse é um exemplo de interesse preguiçoso de avaliação ...
Pode aumentar a eficiência. Este é o que parece óbvio, mas não é realmente o mais importante. (Observe também que a preguiça também pode matar a eficiência - esse fato não é imediatamente óbvio. No entanto, ao armazenar muitos resultados temporários em vez de calculá-los imediatamente, você pode usar uma grande quantidade de RAM.)
Ele permite que você defina construções de controle de fluxo em código normal de nível de usuário, em vez de ser embutido na linguagem. (Por exemplo, Java tem for
loops; Haskell tem uma for
função. Java tem tratamento de exceção; Haskell tem vários tipos de mônada de exceção. C # tem goto
; Haskell tem a mônada de continuação ...)
Ele permite que você desacople o algoritmo para gerar dados do algoritmo para decidir quantos dados gerar. Você pode escrever uma função que gere uma lista de resultados nocionalmente infinita e outra função que processe essa lista tanto quanto for necessário. Mais especificamente, você pode ter cinco funções geradoras e cinco funções de consumidor, e pode produzir qualquer combinação com eficiência - em vez de codificar manualmente 5 x 5 = 25 funções que combinam as duas ações ao mesmo tempo. (!) Todos nós sabemos que a dissociação é uma coisa boa.
Ele mais ou menos força você a projetar uma linguagem funcional pura . É sempre tentador pegar atalhos, mas em uma linguagem preguiçosa, a menor impureza torna seu código totalmente imprevisível, o que milita fortemente contra a tomada de atalhos.
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 anexar duas pilhas xey leva O (comprimento de x) tempo nos casos melhor, pior e médio. Anexar duas pilhas é uma operação monolítica, ela toca todos os nós na pilha x.
Podemos reescrever a estrutura de dados como uma pilha preguiçosa:
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 do código em seu construtor. Uma vez avaliado usando .Force()
, o valor de retorno é armazenado em cache e reutilizado em cada.Force()
.
Com a versão preguiçosa, appends são uma operação O (1): ele retorna 1 nó e suspende a reconstrução real da lista. Ao obter o cabeçalho desta lista, ele avaliará o conteúdo do nó, forçando-o a retornar o cabeçalho e criar uma suspensão com os elementos restantes, portanto, assumir o cabeçalho da lista é uma operação O (1).
Então, nossa lista preguiçosa está em um estado constante de reconstrução, você não paga o custo para reconstruir esta lista até que você atravesse todos os seus elementos. Usando preguiça, esta lista suporta O (1) consing e appending. Curiosamente, uma vez que não avaliamos os nós até que eles sejam acessados, é totalmente possível construir uma lista com elementos potencialmente infinitos.
A estrutura de dados acima não requer que os nós sejam recalculados em cada travessia, portanto, eles são distintamente diferentes dos IEnumerables tradicionais no .NET.
Este snippet mostra a diferença entre avaliação preguiçosa e não preguiçosa. É claro que essa função de fibonacci poderia ser otimizada e usar avaliação preguiçosa em vez de recursão, mas isso estragaria o exemplo.
Vamos supor que PODEMOS ter que usar os 20 primeiros números para alguma coisa, sem avaliação preguiçosa, todos os 20 números devem ser gerados antecipadamente, mas, com avaliação preguiçosa, eles serão gerados apenas conforme necessário. Assim, você pagará apenas o preço de cálculo quando necessário.
Saída de amostra
Geração não preguiçosa: 0,023373 Geração preguiçosa: 0,000009 Saída não lenta: 0,000921 Saída lenta: 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)
A avaliação lenta é mais útil com estruturas de dados. Você pode definir uma matriz ou vetor, especificando indutivamente apenas alguns pontos na estrutura e expressando todos os outros em termos de toda a matriz. Isso permite gerar estruturas de dados de forma muito concisa e com alto desempenho em tempo de execução.
Para ver isso em ação, você pode dar uma olhada em minha biblioteca de rede neural chamada instinto . Faz uso pesado de avaliação preguiçosa para elegância e alto desempenho. Por exemplo, eu me livro totalmente do cálculo de ativação tradicionalmente imperativo. Uma simples expressão preguiçosa faz tudo por mim.
Isso é usado, por exemplo, no função de ativação e também no algoritmo de aprendizado de retropropagação (só posso postar dois links, então você mesmo precisará procurar a learnPat
função no AI.Instinct.Train.Delta
módulo). Tradicionalmente, ambos requerem algoritmos iterativos muito mais complicados.
Outras pessoas já deram todos os grandes motivos, mas acho que um exercício útil para ajudar a entender por que a preguiça é importante é tentar escrever um ponto fixo função de em uma linguagem estrita.
Em Haskell, uma função de ponto fixo é super fácil:
fix f = f (fix f)
isso se expande para
f (f (f ....
mas, como Haskell é preguiçoso, essa cadeia infinita de computação não é problema; a avaliação é feita "de fora para dentro", e tudo funciona perfeitamente:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
É importante ressaltar que não importa que fix
seja preguiçoso, mas que f
seja preguiçoso. Uma vez que você já recebeu um estrito f
, você pode jogar as mãos para o alto e desistir ou eta expandi-lo e desordenar as coisas. (Isso é muito parecido com o que Noah estava dizendo sobre ser a biblioteca que é estrita / preguiçosa, não a linguagem).
Agora imagine escrever a mesma função em Scala estrito:
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)
}
Obviamente, você obtém um estouro de pilha. Se quiser que funcione, você precisa usar o f
argumento call-by-need:
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)
Não sei como você pensa sobre as coisas, mas acho útil pensar na avaliação preguiçosa como um problema de biblioteca em vez de um recurso de linguagem.
Quero dizer que em linguagens restritas, posso implementar a avaliação preguiçosa construindo algumas estruturas de dados, e em linguagens preguiçosas (pelo menos Haskell), posso pedir rigor quando eu quiser. Portanto, a escolha do idioma não torna seus programas preguiçosos ou não preguiçosos, mas simplesmente afeta o que você obtém por padrão.
Depois de pensar nisso dessa forma, pense em todos os lugares onde você escreve uma estrutura de dados que você pode usar mais tarde para gerar dados (sem olhar muito antes disso), e você verá muitos usos para preguiçoso avaliação.
A exploração mais útil da avaliação preguiçosa que usei foi uma função que chamou uma série de subfunções em uma ordem específica. Se qualquer uma dessas subfunções falhou (retornou falso), a função de chamada precisava retornar imediatamente. Então, eu poderia ter feito desta forma:
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;
}
Depois de começar a usá-lo, você verá oportunidades para usá-lo com cada vez mais frequência.
Sem uma avaliação preguiçosa, você não terá permissão para escrever algo como isto:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
Entre outras coisas, as linguagens preguiçosas permitem estruturas de dados infinitas multidimensionais.
Embora o schema, python, etc. permitam estruturas de dados infinitas unidimensionais com fluxos, você só pode atravessar ao longo de uma dimensão.
A preguiça é útil para o mesmo problema marginal , mas vale a pena observar a conexão das corrotinas mencionada naquele link.
A avaliação preguiçosa é o raciocínio equacional do pobre (que se poderia esperar, idealmente, que deduzisse propriedades de código a partir de propriedades de tipos e operações envolvidas).
Exemplo em que funciona muito bem: sum . take 10 $ [1..10000000000]
. Que não nos importamos em 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, isso criaria uma lista gigantesca na memória apenas para usar seus primeiros 10 elementos. Certamente seria muito lento e poderia 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]
. O que na verdade vai somar os 1 000 000 de números, mesmo que em um loop em vez de em uma lista; ainda assim, deve ser reduzido a apenas um cálculo numérico direto, com poucas condicionais e poucas fórmulas. O que seria muito melhor do que somar os 1 000 000 de números. Mesmo se em um loop, e não em uma lista (ou seja, após a otimização do desmatamento).
Outra coisa é que ele torna possível codificar no estilo contras do módulo de recursão da cauda e simplesmente funciona .
cf. resposta relacionada .
Se por "avaliação preguiçosa" você quer dizer como em booleanos combinados, como em
if (ConditionA && ConditionB) ...
então a resposta é simplesmente que quanto menos ciclos de CPU o programa consome, mais rápido ele será executado ... e se um conjunto de instruções de processamento não tiver impacto no resultado do programa, será desnecessário (e, portanto, um desperdício de tempo) para realizá-los de qualquer maneira ...
se otoh, você quer dizer o que eu conheço como "inicializadores lazy", 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 o código do cliente usando a classe para evitar a necessidade de chamar o banco de dados para o registro de dados do Supervisor, exceto quando o cliente que usa o objeto Employee requer acesso aos dados do supervisor ... isso torna o processo de instanciar um Employee mais rápido, e ainda quando você precisar do Supervisor, a primeira chamada para a propriedade Supervisor irá acionar a chamada do Banco de Dados e os dados serão buscados e disponibilizados ...
Trecho de funções de ordem superior
Vamos encontrar o maior número abaixo de 100.000 divisível por 3829. Para fazer isso, vamos apenas filtrar um conjunto de possibilidades nas quais sabemos que está a solução.
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
Primeiro, fazemos uma lista de todos os números inferiores a 100.000, em ordem decrescente. Em seguida, o filtramos por nosso predicado e, como os números são classificados de maneira decrescente, o maior número que satisfaz nosso predicado é o primeiro elemento da lista filtrada. Nem mesmo precisamos usar uma lista finita para nosso conjunto inicial. Isso é preguiça em ação novamente. Como acabamos usando apenas o cabeçalho da lista filtrada, não importa se a lista filtrada é finita ou infinita. A avaliação pára quando a primeira solução adequada é encontrada.