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


119

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:


96

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.


6
Python ha evaluado perezosamente listas infinitas a través de iteradores
Mark Cidade

4
De hecho, puede emular una lista infinita en Python utilizando generadores y expresiones generadoras (que funcionan de manera similar a una lista de comprensión): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
Los generadores hacen que las listas perezosas sean fáciles en Python, pero otras técnicas de evaluación perezosa y estructuras de datos son notablemente menos elegantes.
Peter Burns

3
Me temo que no estaría de acuerdo con esta respuesta. Solía ​​pensar que la pereza se trataba de eficiencia, pero después de haber usado Haskell en una cantidad sustancial, y luego cambiar a Scala y comparar la experiencia, tengo que decir que la pereza es importante a menudo, pero rara vez debido a la eficiencia. Creo que Edward Kmett da con las verdaderas razones.
Owen

3
De manera similar, no estoy de acuerdo, si bien no hay una noción explícita de una lista infinita en C debido a una evaluación estricta, puede jugar fácilmente el mismo truco en cualquier otro idioma (y, de hecho, en la mayoría de la implementación real de todos los lenguajes perezosos) mediante el uso de thunks y la función de transmisión. punteros para trabajar con un prefijo finito de la estructura infinita producida por expresiones similares.
Kristopher Micinski

71

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.


19
De manera más general, take k $ quicksort listsolo 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.
efímero

@ephemient, ¿no te refieres a O (nk log k)?
MaiaVictor

1
@Viclib No, quise decir lo que dije.
Efemiente

@ephemient entonces creo que no lo entiendo, lamentablemente
MaiaVictor

2
@Viclib Un algoritmo de selección para encontrar los primeros k elementos de n es O (n + k log k). Cuando implementa el ordenamiento rápido en un lenguaje diferido y solo lo evalúa lo suficiente para determinar los primeros k elementos (deteniendo la evaluación después), hace exactamente las mismas comparaciones que haría un algoritmo de selección no diferido.
Efemiente

70

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 letque 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).


5
Muy agradable; estas son las verdaderas respuestas. Solía ​​pensar que se trataba de eficiencia (retrasar los cálculos para más adelante) hasta que usé Haskell en una cantidad significativa y vi que esa no era realmente la razón.
Owen

11
Además, aunque no es técnicamente cierto que un lenguaje perezoso deba ser puro (R como ejemplo), es cierto que un lenguaje perezoso impuro puede hacer cosas muy raras (R como ejemplo).
Owen

4
Seguro que lo hay. En un lenguaje estricto, la recursividad letes una bestia peligrosa, en el esquema R6RS permite que #faparezcan aleatorias en su término dondequiera que hacer el nudo lleve estrictamente a un ciclo. Sin juego de palabras, pero los letenlaces estrictamente más recursivos son sensibles en un lenguaje perezoso. La rigurosidad también exacerba el hecho de que whereno 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 #fproblema. Estricto whereadivina su código con preocupaciones no locales.
Edward KMETT

2
¿Podrías explicar cómo la pereza ayuda a evitar la restricción de valor? No he podido entender esto.
Tom Ellis

3
@PaulBone ¿De qué estás hablando? La pereza tiene mucho que ver con las estructuras de control. Si define su propia estructura de control en un lenguaje estricto, tendrá que usar un montón de lambdas o similar, o será una mierda. Porque ifFunc(True, x, y)va a evaluar ambos xy en ylugar de solo x.
punto

28

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


25

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.


La analogía con el GC me ayudó un poco, pero ¿puede dar un ejemplo de "elimina algún código repetitivo", por favor?
Abdul

1
@Abdul, un ejemplo familiar para cualquier usuario de ORM: carga de asociación perezosa. Carga la asociación desde la base de datos "justo a tiempo" y al mismo tiempo libera al desarrollador de la necesidad de especificar explícitamente cuándo cargarla y cómo almacenarla en caché (me refiero a esta repetición). Aquí hay otro ejemplo: projectlombok.org/features/GetterLazy.html .
Alexey

25

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 .


No solo los obligó a mantener el lenguaje puro, sino que también les permitió hacerlo, cuando (antes de la introducción de la IOmónada) la firma de mainsería String -> Stringy ya podría escribir programas correctamente interactivos.
izquierda rotonda sobre el

@leftaroundabout: ¿Qué impide que un lenguaje estricto fuerce todos los efectos en una IOmónada?
Tom Ellis

13

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

  • Una función de generación de movimientos que toma un tablero actual y genera una lista de tableros nuevos, cada uno con un movimiento aplicado.
  • Luego hay una función de "árbol de movimientos" que aplica la función de generación de movimientos para derivar todas las posibles posiciones del tablero que podrían derivarse de esta.
  • Hay una función minimax que recorre el árbol (o posiblemente solo una parte) 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 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.


1
[En un lenguaje "ansioso" (es decir, convencional) esto no funcionará porque el árbol de movimientos no cabe en la memoria] - para Tic-Tac-Toe ciertamente lo hará. Hay como máximo 3 ** 9 = 19683 posiciones para almacenar. Si almacenamos cada uno en 50 bytes extravagantes, eso es casi un megabyte. Eso no es nada ...
Jonas Kölker

6
Sí, ese es mi punto. Los lenguajes ávidos pueden tener una estructura limpia para juegos triviales, pero deben comprometer esa estructura para cualquier cosa real. Los lenguajes perezosos no tienen ese problema.
Paul Johnson

3
Sin embargo, para ser justos, la evaluación perezosa puede provocar sus propios problemas de memoria. No es raro que la gente pregunte por qué Haskell está arruinando su memoria por algo que, en una evaluación ansiosa, tendría un consumo de memoria de O (1)
RHSeeger

@PaulJohnson Si evalúas todas las posiciones, no importa si las evalúas con entusiasmo o con pereza. Debe hacerse el mismo trabajo. Si te detienes en el medio y evalúas solo la mitad de las posiciones, tampoco hace ninguna diferencia, porque en ambos casos hay que hacer la mitad del trabajo. La única diferencia entre las dos evaluaciones es que el algoritmo se ve mejor, si se escribe con pereza.
hasta el

12

Aquí hay dos puntos más que no creo que se hayan planteado todavía en el debate.

  1. 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.

  2. 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.


10

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.


1
Algunas personas podrían decir que eso es realmente una "ejecución perezosa". La diferencia es realmente inmaterial excepto en lenguajes razonablemente puros como Haskell; pero la diferencia es que no solo se retrasa el cálculo, sino también los efectos secundarios asociados (como abrir y leer archivos).
Owen

8

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 ...


Pensé que era un cortocircuito, no una evaluación perezosa.
Thomas Owens

2
Es una evaluación perezosa ya que conditionTwo solo se calcula si es realmente necesaria (es decir, si conditionOne es verdadera).
Romain Linsolas

7
Supongo que el cortocircuito es un caso degenerado de evaluación perezosa, pero definitivamente no es una forma común de pensarlo.
rmeador

19
El cortocircuito es de hecho un caso especial de evaluación perezosa. La evaluación perezosa abarca mucho más que un cortocircuito, obviamente. O, ¿qué tiene el cortocircuito además de la evaluación perezosa?
yfeldblum

2
@Juliet: Tienes una fuerte definición de 'vago'. Su ejemplo de una función que toma dos parámetros no es lo mismo que una declaración if de cortocircuito. Una declaración if de cortocircuito evita cálculos innecesarios. Creo que una mejor comparación con su ejemplo sería el operador de Visual Basic "y también", que obliga a evaluar ambas condiciones

8
  1. 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).

  2. 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 forbucles; Haskell tiene una forfunció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 ...)

  3. 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.

  4. 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.


6

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

lazyfunciona 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.


5

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)

5

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 learnPatfunción en el AI.Instinct.Train.Deltamódulo usted mismo). Tradicionalmente, ambos requieren algoritmos iterativos mucho más complicados.


4

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 fixsea ​​vago, sino que fsea ​​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 fargumento 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)

3

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.


1
La implementación de la evaluación perezosa en lenguajes estrictos es a menudo un Turing Tarpit.
itsbruce

2

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.


2

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

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

Bueno, en mi opinión, esta es una mala idea para hacer eso. Si bien este código puede ser correcto (dependiendo de lo que intente lograr), es difícil de leer, lo que siempre es malo.
Brann

12
No lo creo. Es una construcción estándar en C y sus parientes.
Paul Johnson

Este es un ejemplo de evaluación de cortocircuito, no evaluación perezosa. ¿O es efectivamente lo mismo?
RufusVS

2

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.


2

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 .


1

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 ...


0

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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.