Hace mucho que me pregunto por qué es útil la evaluación perezosa. Todavía tengo que tener a alguien que me explique de una manera que tenga sentido; sobre todo termina reduciéndose a "confía en mí".
Nota: no me refiero a la memorización.
Hace mucho que me pregunto por qué es útil la evaluación perezosa. Todavía tengo que tener a alguien que me explique de una manera que tenga sentido; sobre todo termina reduciéndose a "confía en mí".
Nota: no me refiero a la memorización.
Respuestas:
Principalmente porque puede ser más eficiente: no es necesario calcular los valores si no se van a utilizar. Por ejemplo, puedo pasar tres valores a una función, pero dependiendo de la secuencia de expresiones condicionales, solo se puede usar un subconjunto. En un lenguaje como C, los tres valores se calcularían de todos modos; pero en Haskell, solo se calculan los valores necesarios.
También permite cosas interesantes como listas infinitas. No puedo tener una lista infinita en un lenguaje como C, pero en Haskell, eso no es un problema. Las listas infinitas se utilizan con bastante frecuencia en determinadas áreas de las matemáticas, por lo que puede resultar útil tener la capacidad de manipularlas.
Un ejemplo útil de evaluación perezosa es el uso de quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
Si ahora queremos encontrar el mínimo de la lista, podemos definir
minimum ls = head (quickSort ls)
Lo que primero ordena la lista y luego toma el primer elemento de la lista. Sin embargo, debido a la evaluación perezosa, solo se calcula la cabeza. Por ejemplo, si tomamos el mínimo de la lista, [2, 1, 3,]
quickSort primero filtrará todos los elementos que sean más pequeños que dos. Luego hace quickSort en eso (devolviendo la lista singleton [1]) que ya es suficiente. Debido a la evaluación perezosa, el resto nunca se ordena, lo que ahorra mucho tiempo de cálculo.
Este es, por supuesto, un ejemplo muy simple, pero la pereza funciona de la misma manera para programas que son muy grandes.
Sin embargo, hay una desventaja en todo esto: se vuelve más difícil predecir la velocidad de ejecución y el uso de memoria de su programa. Esto no significa que los programas perezosos sean más lentos o necesiten más memoria, pero es bueno saberlo.
take k $ quicksort list
solo toma O (n + k log k) tiempo, donde n = length list
. Con una clasificación de comparación no perezosa, esto siempre tomaría O (n log n) tiempo.
Encuentro la evaluación perezosa útil para varias cosas.
Primero, todos los lenguajes perezosos existentes son puros, porque es muy difícil razonar sobre los efectos secundarios en un lenguaje perezoso.
Los lenguajes puros le permiten razonar sobre las definiciones de funciones utilizando el razonamiento ecuacional.
foo x = x + 3
Desafortunadamente, en un entorno no perezoso, no se devuelven más declaraciones que en un entorno perezoso, por lo que esto es menos útil en lenguajes como ML. Pero en un lenguaje perezoso puedes razonar con seguridad sobre la igualdad.
En segundo lugar, muchas cosas como la 'restricción de valor' en ML no son necesarias en lenguajes perezosos como Haskell. Esto conduce a una gran ordenación de la sintaxis. Los lenguajes como ML necesitan usar palabras clave como var o fun. En Haskell, estas cosas se reducen a una sola noción.
En tercer lugar, la pereza te permite escribir código muy funcional que se puede entender por partes. En Haskell es común escribir un cuerpo de función 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 = ...
Esto le permite trabajar "de arriba hacia abajo" a través de la comprensión del cuerpo de una función. Los lenguajes similares a ML te obligan a utilizar un lenguaje let
que se evalúa estrictamente. En consecuencia, no se atreve a "levantar" la cláusula let al cuerpo principal de la función, porque si es costosa (o tiene efectos secundarios) no desea que siempre se evalúe. Haskell puede "empujar" los detalles a la cláusula where explícitamente porque sabe que el contenido de esa cláusula solo se evaluará según sea necesario.
En la práctica, tendemos a usar protectores y colapsar que además de:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Cuarto, la pereza a veces ofrece una expresión mucho más elegante de ciertos algoritmos. Una 'clasificación rápida' perezosa en Haskell es una frase simple y tiene la ventaja de que si solo observa los primeros elementos, solo paga costos proporcionales al costo de seleccionar solo esos elementos. Nada le impide hacer esto estrictamente, pero es probable que tenga que volver a codificar el algoritmo cada vez para lograr el mismo rendimiento asintótico.
Quinto, la pereza te permite definir nuevas estructuras de control en el lenguaje. No se puede escribir una nueva construcción similar a "si ... entonces ... más ..." en un lenguaje estricto. Si intenta definir una función como:
if' True x y = x
if' False x y = y
en un lenguaje estricto, ambas ramas se evaluarían independientemente del valor de la condición. Se pone peor cuando se consideran los bucles. Todas las soluciones estrictas requieren que el lenguaje le proporcione algún tipo de cotización o construcción lambda explícita.
Finalmente, en la misma línea, algunos de los mejores mecanismos para lidiar con los efectos secundarios en el sistema de tipos, como las mónadas, realmente solo pueden expresarse de manera efectiva en un entorno perezoso. Esto se puede comprobar comparando la complejidad de los flujos de trabajo de F # con Haskell Monads. (Puede definir una mónada en un lenguaje estricto, pero desafortunadamente a menudo fallará una o dos leyes de mónadas debido a la falta de pereza y los flujos de trabajo en comparación recogen una tonelada de equipaje estricto).
let
es una bestia peligrosa, en el esquema R6RS permite que #f
aparezcan aleatorias en su término dondequiera que hacer el nudo lleve estrictamente a un ciclo. Sin juego de palabras, pero los let
enlaces estrictamente más recursivos son sensibles en un lenguaje perezoso. La rigurosidad también exacerba el hecho de que where
no tiene forma de ordenar los efectos relativos en absoluto, excepto por SCC, es una construcción a nivel de declaración, sus efectos podrían ocurrir en cualquier orden estrictamente, e incluso si tiene un lenguaje puro, termina con el #f
problema. Estricto where
adivina su código con preocupaciones no locales.
ifFunc(True, x, y)
va a evaluar ambos x
y en y
lugar de solo x
.
Hay una diferencia entre la evaluación de orden normal y la evaluación perezosa (como en Haskell).
square x = x * x
Evaluando la siguiente expresión ...
square (square (square 2))
... con una evaluación entusiasta:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
... con evaluación 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
... con evaluación perezosa:
> (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
Eso es porque la evaluación perezosa mira el árbol de sintaxis y hace transformaciones de árbol ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... mientras que la evaluación de orden normal solo hace expansiones textuales.
Es por eso que, cuando usamos la evaluación perezosa, nos volvemos más poderosos (la evaluación termina con más frecuencia que otras estrategias) mientras que el rendimiento es equivalente a una evaluación entusiasta (al menos en notación O).
La evaluación perezosa relacionada con la CPU de la misma manera que la recolección de basura relacionada con la RAM. GC le permite fingir que tiene una cantidad ilimitada de memoria y, por lo tanto, solicitar tantos objetos en la memoria como necesite. Runtime recuperará automáticamente los objetos inutilizables. LE le permite fingir que tiene recursos computacionales ilimitados; puede hacer tantos cálculos como necesite. El tiempo de ejecución simplemente no ejecutará cálculos innecesarios (para un caso dado).
¿Cuál es la ventaja práctica de estos modelos "fingidos"? Libera al desarrollador (hasta cierto punto) de la gestión de recursos y elimina algunos códigos repetitivos de sus fuentes. Pero lo más importante es que puede reutilizar su solución de manera eficiente en un conjunto más amplio de contextos.
Imagina que tienes una lista de números S y un número N. Necesitas encontrar el número M más cercano al número N de la lista S. Puedes tener dos contextos: N único y alguna lista L de Ns (ei para cada N en L busca la M más cercana en S). Si usa la evaluación perezosa, puede ordenar S y aplicar la búsqueda binaria para encontrar la M más cercana a la N.Para una buena clasificación perezosa, se requerirán pasos de O (tamaño (S)) para N y O (ln (tamaño (S)) * (tamaño (S) + tamaño (L))) pasos para L. distribuidos equitativamente. Si no tiene una evaluación perezosa para lograr la eficiencia óptima, debe implementar un algoritmo para cada contexto.
Si le cree a Simon Peyton Jones, la evaluación perezosa no es importante per se, sino solo como una 'camisa de pelo' que obligó a los diseñadores a mantener el lenguaje puro. Me siento comprensivo con este punto de vista.
Richard Bird, John Hughes y, en menor medida, Ralf Hinze son capaces de hacer cosas asombrosas con una evaluación perezosa. Leer su trabajo te ayudará a apreciarlo. Un buen punto de partida es el magnífico solucionador de Sudoku de Bird y el artículo de Hughes sobre Por qué es importante la programación funcional .
IO
mónada) la firma de main
sería String -> String
y ya podría escribir programas correctamente interactivos.
IO
mónada?
Considere un programa de tic-tac-toe. Esto tiene cuatro funciones:
Esto crea una clara separación de preocupaciones. En particular, la función de generación de movimientos y las funciones de evaluación del tablero son las únicas que necesitan comprender las reglas del juego: el árbol de movimientos y las funciones minimax son completamente reutilizables.
Ahora intentemos implementar el ajedrez en lugar de tic-tac-toe. En un lenguaje "ansioso" (es decir, convencional) esto no funcionará porque el árbol de movimientos no cabe en la memoria. Así que ahora las funciones de evaluación de la placa y generación de movimientos deben mezclarse con el árbol de movimientos y la lógica del minimax porque la lógica del minimax debe usarse para decidir qué movimientos generar. Nuestra bonita estructura modular limpia desaparece.
Sin embargo, en un lenguaje perezoso, los elementos del árbol de movimientos solo se generan en respuesta a las demandas de la función minimax: no es necesario generar el árbol de movimientos completo antes de soltar el minimax en el elemento superior. Entonces, nuestra estructura modular limpia todavía funciona en un juego real.
Aquí hay dos puntos más que no creo que se hayan planteado todavía en el debate.
La pereza es un mecanismo de sincronización en un entorno concurrente. Es una forma sencilla y ligera de crear una referencia a algún cálculo y compartir sus resultados entre muchos subprocesos. Si varios subprocesos intentan acceder a un valor no evaluado, solo uno de ellos lo ejecutará y los demás se bloquearán en consecuencia, recibiendo el valor una vez que esté disponible.
La pereza es fundamental para amortizar las estructuras de datos en un entorno puro. Okasaki describe esto en detalle en Estructuras de datos puramente funcionales , pero la idea básica es que la evaluación perezosa es una forma controlada de mutación crítica para permitirnos implementar ciertos tipos de estructuras de datos de manera eficiente. Si bien a menudo hablamos de la pereza que nos obliga a usar la caperuza de pureza, también se aplica lo contrario: son un par de características de lenguaje sinérgicas.
Cuando enciende su computadora y Windows se abstiene de abrir cada directorio en su disco duro en el Explorador de Windows y se abstiene de iniciar cada programa instalado en su computadora, hasta que usted indique que se necesita un directorio determinado o un programa determinado, que es una evaluación "perezosa".
La evaluación "perezosa" consiste en realizar operaciones cuando y como se necesitan. Es útil cuando es una característica de un lenguaje de programación o biblioteca porque generalmente es más difícil implementar una evaluación perezosa por su cuenta que simplemente precalcular todo por adelantado.
Considera esto:
if (conditionOne && conditionTwo) {
doSomething();
}
El método doSomething () se ejecutará solo si conditionOne es verdadera y conditionTwo es verdadera. En el caso de que conditionOne sea falsa, ¿por qué necesita calcular el resultado de conditionTwo? La evaluación de conditionTwo será una pérdida de tiempo en este caso, especialmente si su condición es el resultado de algún proceso de método.
Ese es un ejemplo del interés perezoso de la evaluación ...
Puede aumentar la eficiencia. Este es el que parece obvio, pero en realidad no es el más importante. (Tenga en cuenta también que la pereza también puede matar la eficiencia; este hecho no es inmediatamente obvio. Sin embargo, al almacenar muchos resultados temporales en lugar de calcularlos de inmediato, puede usar una gran cantidad de RAM).
Le permite definir construcciones de control de flujo en código normal a nivel de usuario, en lugar de estar codificado en el lenguaje. (Por ejemplo, Java tiene for
bucles; Haskell tiene una for
función. Java tiene manejo de excepciones; Haskell tiene varios tipos de mónada de excepción. C # tiene goto
; Haskell tiene la mónada de continuación ...)
Le permite desacoplar el algoritmo para generar datos del algoritmo para decidir cuántos datos generar. Puede escribir una función que genere una lista de resultados teóricamente infinita y otra función que procese tanto de esta lista como decida que necesita. Más concretamente, puede tener cinco funciones de generador y cinco funciones de consumidor, y puede producir de manera eficiente cualquier combinación, en lugar de codificar manualmente 5 x 5 = 25 funciones que combinan ambas acciones a la vez. (!) Todos sabemos que el desacoplamiento es algo bueno.
Te obliga más o menos a diseñar un lenguaje funcional puro . Siempre es tentador tomar atajos, pero en un lenguaje perezoso, la más mínima impureza hace que su código sea tremendamente impredecible, lo que milita fuertemente en contra de tomar atajos.
Un gran beneficio de la pereza es la capacidad de escribir estructuras de datos inmutables con límites amortizados razonables. Un ejemplo simple es una pila inmutable (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)
El código es razonable, pero agregar dos pilas xey toma O (longitud de x) en los casos mejores, peores y promedio. Agregar dos pilas es una operación monolítica, toca todos los nodos en la pila x.
Podemos reescribir la estructura de datos como una pila perezosa:
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 suspendiendo la evaluación del código en su constructor. Una vez evaluado el uso .Force()
, el valor de retorno se almacena en caché y se reutiliza en cada subsiguiente .Force()
.
Con la versión diferida, los anexos son una operación O (1): devuelve 1 nodo y suspende la reconstrucción real de la lista. Cuando obtenga el encabezado de esta lista, evaluará el contenido del nodo, lo que obligará a devolver el encabezado y crear una suspensión con los elementos restantes, por lo que tomar el encabezado de la lista es una operación O (1).
Por lo tanto, nuestra lista perezosa se encuentra en un estado constante de reconstrucción, no paga el costo de reconstruir esta lista hasta que recorre todos sus elementos. Usando la pereza, esta lista apoya O (1) consing y anexar. Curiosamente, dado que no evaluamos los nodos hasta que se accede a ellos, es totalmente posible construir una lista con elementos potencialmente infinitos.
La estructura de datos anterior no requiere que los nodos se vuelvan a calcular en cada recorrido, por lo que son claramente diferentes de los IEnumerables básicos en .NET.
Este fragmento muestra la diferencia entre la evaluación perezosa y no perezosa. Por supuesto, esta función de fibonacci podría optimizarse y utilizar la evaluación perezosa en lugar de la recursividad, pero eso estropearía el ejemplo.
Supongamos que PODEMOS tener que usar los 20 primeros números para algo, sin una evaluación perezosa, todos los 20 números deben generarse por adelantado, pero con la evaluación perezosa se generarán solo cuando sea necesario. Por lo tanto, solo pagará el precio de cálculo cuando sea necesario.
Salida de muestra
Generación no perezosa: 0.023373 Generación perezosa: 0.000009 Salida no perezosa: 0.000921 Salida perezosa: 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)
La evaluación perezosa es más útil con estructuras de datos. Puede definir una matriz o vector especificando inductivamente solo ciertos puntos en la estructura y expresando todos los demás en términos de toda la matriz. Esto le permite generar estructuras de datos de forma muy concisa y con un alto rendimiento en tiempo de ejecución.
Para ver esto en acción, puede echar un vistazo a mi biblioteca de redes neuronales llamada instinto . Hace un uso intensivo de la evaluación perezosa para obtener elegancia y alto rendimiento. Por ejemplo, me deshago totalmente del cálculo de activación tradicionalmente imperativo. Una simple expresión perezosa hace todo por mí.
Esto se utiliza, por ejemplo, en la función de activación. y también en el algoritmo de aprendizaje de retropropagación (solo puedo publicar dos enlaces, por lo que deberá buscar la learnPat
función en el AI.Instinct.Train.Delta
módulo usted mismo). Tradicionalmente, ambos requieren algoritmos iterativos mucho más complicados.
Otras personas ya dieron todas las razones importantes, pero creo que un ejercicio útil para ayudar a comprender por qué es importante la pereza es intentar escribir un punto fijo función de en un lenguaje estricto.
En Haskell, una función de punto fijo es muy fácil:
fix f = f (fix f)
esto se expande a
f (f (f ....
pero debido a que Haskell es vago, esa cadena infinita de cómputo no es un problema; la evaluación se realiza "de afuera hacia adentro", y todo funciona de maravilla:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
Es importante destacar que no importa que fix
sea vago, sino que f
sea vago. Una vez que ya te hayan dado un estricto f
, puedes lanzar tus manos al aire y rendirte, o expandirlo y desordenar las cosas. (Esto es muy parecido a lo que Noah estaba diciendo acerca de que es la biblioteca la que es estricta / perezosa, no el lenguaje).
Ahora imagina escribir la misma función en Scala estricto:
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)
}
Por supuesto, obtienes un desbordamiento de pila. Si desea que funcione, debe hacer que el f
argumento sea llamado por necesidad:
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)
No sé cómo piensa actualmente sobre las cosas, pero me parece útil pensar en la evaluación perezosa como un problema de biblioteca en lugar de una característica del lenguaje.
Quiero decir que en lenguajes estrictos, puedo implementar la evaluación perezosa construyendo algunas estructuras de datos, y en lenguajes perezosos (al menos Haskell), puedo pedir rigor cuando lo desee. Por lo tanto, la elección del idioma no hace que sus programas sean perezosos o no perezosos, sino que simplemente afecta el que obtiene de forma predeterminada.
Una vez que lo piense así, piense en todos los lugares donde escribe una estructura de datos que luego puede usar para generar datos (sin mirarla demasiado antes), y verá muchos usos para lazy evaluación.
La explotación más útil de la evaluación perezosa que he usado fue una función que llamaba a una serie de subfunciones en un orden particular. Si alguna de estas subfunciones fallaba (devolvía falso), la función de llamada debía regresar inmediatamente. Entonces podría haberlo hecho de esta manera:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
o, la solución más elegante:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
Una vez que comience a usarlo, verá oportunidades para usarlo cada vez con más frecuencia.
Sin una evaluación perezosa, no se le permitirá escribir algo como esto:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
Entre otras cosas, los lenguajes perezosos permiten estructuras de datos infinitas multidimensionales.
Si bien el esquema, python, etc., permiten estructuras de datos infinitas unidimensionales con flujos, solo puede atravesar una dimensión.
La pereza es útil para el mismo problema marginal , pero vale la pena señalar la conexión de corrutinas mencionada en ese enlace.
La evaluación perezosa es el razonamiento ecuacional del pobre (que se podría esperar, idealmente, que deduzca propiedades del código de las propiedades de los tipos y operaciones involucradas).
Ejemplo donde funciona bastante bien: sum . take 10 $ [1..10000000000]
. Lo cual no nos importa que se reduzca a una suma de 10 números, en lugar de un solo cálculo numérico directo y simple. Sin la evaluación perezosa, por supuesto, esto crearía una lista gigantesca en la memoria solo para usar sus primeros 10 elementos. Ciertamente sería muy lento y podría causar un error de memoria insuficiente.
Ejemplo donde no es tan grande como nos gustaría: sum . take 1000000 . drop 500 $ cycle [1..20]
. Lo que en realidad sumará los 1 000 000 de números, incluso si están en un bucle en lugar de en una lista; aun así, debería reducirse a un solo cálculo numérico directo, con pocos condicionales y pocas fórmulas. Lo que sería mucho mejor que sumar los 1 000 000 de números. Incluso si está en un bucle y no en una lista (es decir, después de la optimización de la deforestación).
Otra cosa es que hace posible codificar en estilo cola recursividad módulo cons , y simplemente funciona .
cf. respuesta relacionada .
Si por "evaluación perezosa" te refieres como en booleanos combinados, como en
if (ConditionA && ConditionB) ...
entonces la respuesta es simplemente que cuantos menos ciclos de CPU consuma el programa, más rápido se ejecutará ... y si una parte de las instrucciones de procesamiento no tendrá ningún impacto en el resultado del programa, entonces no es necesario (y por lo tanto un desperdicio de tiempo) para realizarlos de todos modos ...
si otoh, te refieres a lo que he conocido como "inicializadores perezosos", como en:
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));
}
}
}
Bueno, esta técnica permite que el código del cliente use la clase para evitar la necesidad de llamar a la base de datos para el registro de datos del Supervisor, excepto cuando el cliente que usa el objeto Empleado requiere acceso a los datos del supervisor ... esto hace que el proceso de instanciar un Empleado sea más rápido, y, sin embargo, cuando necesite el supervisor, la primera llamada a la propiedad del supervisor activará la llamada a la base de datos y los datos se recuperarán y estarán disponibles ...
Extracto de funciones de orden superior
Encontremos el número más grande debajo de 100,000 que sea divisible por 3829. Para hacer eso, simplemente filtraremos un conjunto de posibilidades en las que sabemos que se encuentra la solución.
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
Primero hacemos una lista de todos los números inferiores a 100.000, descendente. Luego lo filtramos por nuestro predicado y debido a que los números están ordenados de manera descendente, el número más grande que satisface nuestro predicado es el primer elemento de la lista filtrada. Ni siquiera necesitábamos usar una lista finita para nuestro set inicial. Eso es de nuevo la pereza en acción. Debido a que solo terminamos usando el encabezado de la lista filtrada, no importa si la lista filtrada es finita o infinita. La evaluación se detiene cuando se encuentra la primera solución adecuada.