it-swarm.dev

¿Por qué es útil la evaluación perezosa?

Hace tiempo que me pregunto por qué la evaluación perezosa es útil. Todavía no tengo a nadie que me explique de una manera que tenga sentido; En su mayoría termina por reducirse a "confía en mí".

Nota: no me refiero a la memoización.

110
Joel McCracken

Sobre todo porque puede ser más eficiente: los valores no necesitan computarse 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 computarían de todos modos; pero en Haskell, solo se calculan los valores necesarios.

También permite cosas geniales como listas infinitas. No puedo tener una lista infinita en un lenguaje como C, pero en Haskell no hay problema. Las listas infinitas se usan con bastante frecuencia en ciertas áreas de las matemáticas, por lo que puede ser útil tener la capacidad de manipularlas.

90
mipadi

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)

Que primero ordena la lista y luego toma el primer elemento de la lista. Sin embargo, debido a la evaluación perezosa, solo se computa la cabeza. Por ejemplo, si tomamos el mínimo de la lista [2, 1, 3,] quickSort, primero filtraremos todos los elementos que sean más pequeños que dos. Luego, realiza un pedido rápido (devolviendo la lista de singleton [1]) que ya es suficiente. Debido a la evaluación perezosa, el resto nunca se clasifica, lo que ahorra mucho tiempo de cómputo.

Este es, por supuesto, un ejemplo muy simple, pero la pereza funciona de la misma manera para programas que son muy grandes.

Sin embargo, todo esto tiene un inconveniente: 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 requieran más memoria, pero es bueno saberlo.

69
Chris Eidhof

Encuentro la evaluación perezosa útil para una serie de cosas.

Primero, todos los lenguajes perezosos existentes son puros, porque es muy difícil razonar acerca de los efectos secundarios en un lenguaje perezoso.

Los lenguajes puros le permiten razonar acerca de las definiciones de funciones usando el razonamiento ecuacional.

foo x = x + 3

Desafortunadamente, en una configuración no perezosa, faltan más declaraciones que en una configuración perezosa, por lo que esto es menos útil en idiomas 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 un gran decluttering de sintaxis. ML como idiomas necesitan usar palabras clave como var o fun. En Haskell estas cosas se colapsan en una sola idea.

Tercero, la pereza le permite escribir código muy funcional que se puede entender en 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 abajo' a través de la comprensión del cuerpo de una función. Los lenguajes similares a ML te obligan a usar un permiso que se evalúa estrictamente. En consecuencia, no se atreve a "levantar" la cláusula de dejar el cuerpo principal de la función, porque si es costoso (o tiene efectos secundarios), no quiere 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 guardas y colapso para:

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 'ordenación rápida' perezosa en Haskell es de una sola línea y tiene el beneficio de que si solo observa los primeros artículos, solo pagará los costos proporcionales al costo de seleccionar solo esos artículos. Nada le impide hacer esto estrictamente, pero es probable que tenga que recodificar el algoritmo cada vez para lograr el mismo rendimiento asintótico.

Quinto, la pereza le permite definir nuevas estructuras de control en el lenguaje. No se puede escribir un nuevo 'si .. entonces .. else ..' como construir en un lenguaje estricto. Si intentas definir una función como:

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

en un lenguaje estricto, entonces ambas ramas se evaluarían independientemente del valor de la condición. Se pone peor cuando se consideran bucles. Todas las soluciones estrictas requieren que el lenguaje le brinde algún tipo de presupuesto o construcción explícita de lambda.

Finalmente, en la misma línea, algunos de los mejores mecanismos para tratar los efectos secundarios en el sistema de tipos, como las mónadas, solo se pueden expresar efectivamente en un entorno perezoso. Esto se puede observar comparando la complejidad de los flujos de trabajo de F # con las mónadas Haskell. (Puede definir una mónada en un lenguaje estricto, pero desafortunadamente, con frecuencia fallará una ley de mónada o dos debido a la falta de pereza y los flujos de trabajo, en comparación, recoger una tonelada de equipaje estricto).

64
Edward KMETT

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 ansiosa evaluación:

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

... con una evaluación de orden 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

Esto se debe a que 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 nosotros, cuando utilizamos la evaluación perezosa, obtenemos más poder (la evaluación termina con más frecuencia que otras estrategias) mientras que el desempeño es equivalente a la evaluación impaciente (al menos en notación O).

28
Thomas Danecker

La evaluación perezosa se relaciona con la CPU de la misma manera que la recolección de basura relacionada con la RAM. GC te permite fingir que tienes una cantidad ilimitada de memoria y, por lo tanto, solicitar tantos objetos en memoria como necesites. Runtime reclamará automáticamente los objetos inutilizables. LE le permite simular que tiene recursos computacionales ilimitados; puede hacer tantos cómputos 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 "simulados"? Libera al desarrollador (hasta cierto punto) de la administración de recursos y elimina parte de su código fuente. Pero lo más importante es que puede reutilizar eficientemente su solución en un conjunto más amplio de contextos.

Imagina que tienes una lista de números S y una N. Necesitas encontrar el más cercano al número N número M de la lista S. Puedes tener dos contextos: una sola N 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 M más cercana a N. Para una buena clasificación perezosa, se requerirán O(size(S)) pasos para una 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 el algoritmo para cada contexto.

25
Alexey

Si crees en 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 simpatizante de 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. Los buenos puntos de partida son el magnífico Sudoku de Bird solucionador y el artículo de Hughes sobre Por qué la programación funcional importa .

25
Norman Ramsey

Considere un programa de tic-tac-toe. Esto tiene cuatro funciones:

  • Una función de generación de movimientos que toma una tabla actual y genera una lista de tablas nuevas cada una con un movimiento aplicado.
  • Luego hay una función de "árbol de movimiento" que aplica la función de generación de movimiento para derivar todas las posiciones posibles del tablero que podrían seguir de este.
  • Hay una función minimax que recorre el árbol (o posiblemente solo una parte de él) para encontrar el mejor próximo movimiento.
  • Hay una función de evaluación del tablero que determina si uno de los jugadores ha ganado.

Esto crea una separación clara de preocupaciones agradable. En particular, la función de generación de movimientos y las funciones de evaluación de la placa son las únicas que necesitan entender las reglas del juego: las funciones de árbol de movimiento y minimax son completamente reutilizables.

Ahora intentemos implementar ajedrez en lugar de tic-tac-toe. En un lenguaje "ansioso" (es decir, convencional) esto no funcionará porque el árbol de movimiento no cabrá en la memoria. Así que ahora las funciones de evaluación de tablero y generación de movimiento deben combinarse con el árbol de movimiento y la lógica minimax, ya que la lógica minimax debe usarse para decidir qué movimientos generar. Nuestra estructura modular limpia Niza desaparece.

Sin embargo, en un lenguaje perezoso, los elementos del árbol de movimiento solo se generan en respuesta a las demandas de la función minimax: no es necesario que se genere todo el árbol de movimiento antes de que dejemos minimax en el elemento superior. Así que nuestra estructura modular limpia todavía funciona en un juego real.

13
Paul Johnson

Aquí hay dos puntos más que creo que aún no se han mencionado en la discusión.

  1. La pereza es un mecanismo de sincronización en un entorno concurrente. Es una forma liviana y fácil de crear una referencia a algunos cálculos 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 otros se bloquearán en consecuencia, recibiendo el valor una vez que esté disponible.

  2. La pereza es fundamental para amortizar las estructuras de datos en un entorno puro. Okasaki describe esto en Estructuras de datos puramente funcionales en detalle, 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 camiseta de pureza, la otra manera también se aplica: son un par de características de lenguaje sinérgicas.

12
Edward Z. Yang

Cuando enciende su computadora, Windows se abstiene de abrir todos los directorios de su disco duro en el Explorador de Windows y se abstiene de iniciar todos los programas instalados en su computadora, hasta que indique que se necesita un directorio determinado o que se necesita un programa determinado, que Es la evaluación "perezosa".

La evaluación "perezosa" es realizar operaciones cuando son necesarias. Es útil cuando se trata de una característica de un lenguaje de programación o biblioteca, ya que generalmente es más difícil implementar una evaluación perezosa por su cuenta que simplemente calcular previamente todo.

9
yfeldblum
  1. Puede aumentar la eficiencia. Este es el de apariencia obvia, pero en realidad no es el más importante. (Tenga en cuenta también que la pereza puede matar eficiencia también - este hecho no es inmediatamente obvio. Sin embargo, al almacenar muchos resultados temporales en lugar de calcularlos inmediatamente, puede utilizar una gran cantidad de RAM).

  2. Le permite definir construcciones de control de flujo en código de nivel de usuario normal, en lugar de ser codificado en el lenguaje. (Por ejemplo, Java tiene for loops; Haskell tiene una función for. 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 ...)

  3. Le permite desacoplar el algoritmo para generar datos del algoritmo para decidir cuánto datos para generar. Puede escribir una función que genere una lista de resultados infinitamente teórica, y otra función que procese la mayor cantidad de esta lista que decida que necesita. Más al punto, puede tener cinco funciones del generador y cinco funciones del 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.

  4. Más o menos te obliga a diseñar un lenguaje funcional puro. Siempre es tentador tomar atajos, pero en un lenguaje perezoso, la impureza más leve hace que tu código salvajemente impredecible, lo que milita fuertemente en contra de tomar atajos.

8

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 si se agregan dos pilas x e y, lleva el tiempo O (longitud de x) en los casos mejor, peor y promedio. Anexar 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 con .Force(), el valor de retorno se almacena en caché y se reutiliza en cada .Force() posterior.

Con la versión perezosa, los apéndices 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 forzará a devolver el encabezado y creará una suspensión con los elementos restantes, por lo que tomar el encabezado de la lista es un O(1) operación.

Por lo tanto, nuestra lista perezosa está en un estado constante de reconstrucción, usted no paga el costo de la reconstrucción de esta lista hasta que recorre todos sus elementos. Al usar la pereza, esta lista admite O(1) considerar y anexar. Curiosamente, ya 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 de Vanilla en .NET.

6
Juliet

Considera esto:

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

El método doSomething () se ejecutará solo si conditionOne es verdadero y conditionTwo es verdadero. En el caso de que conditionOne sea falso, ¿por qué necesita calcular el resultado de la condición Dos? La evaluación de la condición Dos será una pérdida de tiempo en este caso, especialmente si su condición es el resultado de algún proceso del método.

Ese es un ejemplo del interés perezoso de la evaluación ...

6
Romain Linsolas

La evaluación perezosa es más útil con las estructuras de datos. Puede definir una matriz o un vector de forma inductiva especificando 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 red neuronal llamada instinto . Hace un uso intensivo de la evaluación perezosa por su 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 usa, por ejemplo, en función de activación y también en el algoritmo de aprendizaje de la propagación hacia atrás (solo puedo publicar dos enlaces, por lo que tendrá que buscar la función learnPat en el módulo AI.Instinct.Train.Delta). Tradicionalmente, ambos requieren algoritmos iterativos mucho más complicados.

5
ertes

Otras personas ya dieron todas las razones importantes, pero creo que un ejercicio útil para ayudar a entender por qué es importante la pereza es tratar de escribir una función punto fijo en un lenguaje estricto.

En Haskell, una función de punto fijo es súper fácil:

fix f = f (fix f)

esto se expande a

f (f (f ....

pero como Haskell es perezoso, esa cadena infinita de cómputo no es un problema; la evaluación se realiza "de afuera a 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 perezoso, sino que f sea vago. Una vez que ya te han dado un f estricto, puedes lanzar las manos al aire y renunciar, o expandirlo y desordenar cosas. (Esto es muy parecido a lo que Noah estaba diciendo acerca de que es la biblioteca que es estricta/perezosa, no el idioma).

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)
}

Usted por supuesto tiene un desbordamiento de pila. Si desea que funcione, debe hacer que el argumento f 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)
4
Owen

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 una evaluación perezosa en lugar de una recursión, pero eso arruinaría el ejemplo.

Supongamos que PUEDE tenemos que usar los 20 primeros números para algo, sin una evaluación perezosa todos los 20 números se deben generar por adelantado, pero, con una 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 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)
4
Vinko Vrsalovic

No sé cómo piensa actualmente de las cosas, pero me parece útil pensar en la evaluación perezosa como un problema de biblioteca en lugar de una función de idioma.

Quiero decir que en idiomas estrictos, puedo implementar la evaluación perezosa mediante la construcción de unas pocas estructuras de datos, y en idiomas perezosos (al menos Haskell), puedo pedir rigor cuando lo deseo. Por lo tanto, la elección del idioma no hace que sus programas sean perezosos o no perezosos, sino que simplemente afecta a lo que obtiene por defecto.

Una vez que lo pienses así, piensa en todos los lugares donde escribes una estructura de datos que luego puedes usar para generar datos (sin mirarlos demasiado antes) y verás muchos usos para los perezosos. evaluación.

3
Noah Lavine

Sin una evaluación perezosa no se te permitirá escribir algo como esto:

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

La explotación más útil de la evaluación perezosa que he usado es una función que llama una serie de subfunciones en un orden particular. Si alguna de estas subfunciones falla (devuelve falso), la función de llamada debe regresar inmediatamente. Así que 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 más a menudo.

2
Marc Bernier

Entre otras cosas, los lenguajes perezosos permiten estructuras de datos infinitos multidimensionales.

Si bien el esquema, python, etc. permite estructuras de datos infinitos 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 coroutines mencionada en ese enlace.

2
shapr

La evaluación perezosa es el razonamiento ecuacional del hombre pobre (que podría esperarse, idealmente, deducir las propiedades del código de las propiedades de los tipos y operaciones involucradas).

Ejemplo donde funciona bastante bien: sum . take 10 $ [1..10000000000]. Lo que no nos importa que sea reducido a una suma de 10 números, en lugar de un solo cálculo numérico directo y simple. Por supuesto, sin la perezosa evaluación, esto crearía una lista gigantesca en la memoria solo para usar sus primeros 10 elementos. Sin duda, sería muy lento y podría causar un error de memoria insuficiente.

Ejemplo donde no es tan bueno como nos gustaría: sum . take 1000000 . drop 500 $ cycle [1..20]. Lo que realmente sumará los 1 000 000 números, incluso si está en un bucle en lugar de en una lista; aún así debería reducirse a solo un cálculo numérico directo, con pocos condicionales y pocas fórmulas. Lo que sería mucho mejor que resumir los 1 000 000 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 de recursión de la cola contras estilo, y simplemente funciona .

cf. respuesta relacionada .

2
Will Ness

Si por "evaluación perezosa" quieres decir 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 un trozo de instrucciones de procesamiento no tendrá ningún impacto en el resultado del programa, entonces es innecesario (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 que usa la clase evite 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 creación de instancias de un Empleado sea más rápido. y sin embargo, cuando necesite el Supervisor, la primera llamada a la propiedad Supervisor activará la llamada de la Base de datos y los datos se buscarán y estarán disponibles ...

1
Charles Bretana

Extracto de Funciones de orden superior

Encontremos el número más grande por debajo de 100,000 que es divisible por 3829. Para hacer eso, simplemente filtraremos un conjunto de posibilidades en las que sabemos que la solución está.

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, descendiendo. Luego lo filtramos por nuestro predicado y debido a que los números se clasifican de manera descendente, el número más grande que satisface nuestro predicado es el primer elemento de la lista filtrada. Ni siquiera tuvimos que usar una lista finita para nuestro set inicial. Eso es pereza en acción otra vez. Como 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.

0
onmyway133