¿Por qué no se usa la evaluación perezosa en todas partes?

32

Acabo de aprender cómo funciona la evaluación diferida y me preguntaba: ¿por qué no se aplica la evaluación diferida en cada software que se produce actualmente? ¿Por qué sigue usando una evaluación entusiasta?

John Smith
fuente
2
Aquí hay un ejemplo de lo que puede suceder si combina estado mutable y evaluación perezosa. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström
2
@ JonasElfström: Por favor, no confunda el estado mutable con una de sus posibles implementaciones. El estado mutable se puede implementar usando una secuencia de valores infinita y lenta. Entonces no tienes el problema de las variables mutables.
Giorgio
En los lenguajes de programación imperativos, la "evaluación perezosa" requiere un esfuerzo consciente del programador. La programación genérica en lenguajes imperativos ha facilitado esto, pero nunca será transparente. La respuesta al otro lado de la pregunta saca otra pregunta: "¿Por qué no se usan lenguajes de programación funcionales en todas partes?", Y la respuesta actual es simplemente "no" como una cuestión de actualidad.
rwong
2
Los lenguajes de programación funcional no se usan en todas partes por la misma razón por la que no usamos martillos en los tornillos, no todos los problemas pueden expresarse fácilmente de una manera funcional de entrada -> salida, la GUI, por ejemplo, es más adecuada para expresarse de manera imperativa .
ALXGTV
Además, hay dos clases de lenguajes de programación funcionales (o al menos ambos afirman ser funcionales), los lenguajes funcionales imperativos, por ejemplo, Clojure, Scala y el declarativo, por ejemplo, Haskell, OCaml.
ALXGTV

Respuestas:

38

La evaluación perezosa requiere gastos generales de contabilidad; debe saber si ya se ha evaluado y esas cosas. La evaluación ansiosa siempre se evalúa, por lo que no tiene que saberlo. Esto es especialmente cierto en contextos concurrentes.

En segundo lugar, es trivial convertir una evaluación entusiasta en una evaluación perezosa empaquetándola en un objeto de función que se llamará más adelante, si así lo desea.

En tercer lugar, la evaluación perezosa implica una pérdida de control. ¿Qué pasa si evalúo perezosamente la lectura de un archivo de un disco? ¿O tomando el tiempo? Eso no es aceptable.

La evaluación entusiasta puede ser más eficiente y más controlable, y se convierte trivialmente en evaluación perezosa. ¿Por qué querrías una evaluación perezosa?

DeadMG
fuente
10
Leer perezosamente un archivo desde el disco es realmente genial: para la mayoría de mis programas y scripts simples, Haskell readFilees exactamente lo que necesito. Además, la conversión de la evaluación perezosa a la ansiosa es igual de trivial.
Tikhon Jelvis
3
De acuerdo con todos ustedes, excepto el último párrafo. La evaluación perezosa es más eficiente cuando hay una operación de la cadena, y puede tener un mayor control de cuando realmente se necesita los datos
texasbruce
44
Las leyes de los funtores quisieran hablar con usted sobre la "pérdida de control". Si escribe funciones puras que operan en tipos de datos inmutables, la evaluación diferida es una bendición. Idiomas como el haskell se basan fundamentalmente en el concepto de la pereza. Es engorroso en algunos idiomas, especialmente cuando se mezcla con código "inseguro", pero está haciendo que parezca que la pereza es peligrosa o mala por defecto. Es solo "peligroso" en código peligroso.
Sara
1
@DeadMG No si te importa si tu código termina o no ... ¿Qué head [1 ..]te da en un lenguaje puro evaluado con entusiasmo, porque en Haskell sí 1?
punto
1
Para muchos idiomas, la implementación de una evaluación diferida al menos introducirá complejidad. A veces se necesita esa complejidad, y tener una evaluación perezosa mejora la eficiencia general, particularmente si lo que se está evaluando solo se necesita condicionalmente. Sin embargo, si se hace mal, puede introducir errores sutiles o problemas de rendimiento difíciles de explicar debido a suposiciones erróneas al escribir el código. Hay una compensación.
Berin Loritsch
17

Principalmente porque el código diferido y el estado pueden mezclarse mal y causar algunos errores difíciles de encontrar. Si el estado de un objeto dependiente cambia, el valor de su objeto vago puede ser incorrecto cuando se evalúa. Es mucho mejor que el programador codifique explícitamente el objeto para que sea perezoso cuando sepa que la situación es apropiada.

En una nota al margen, Haskell usa la evaluación Lazy para todo. Esto es posible porque es un lenguaje funcional y no usa estado (excepto en algunas circunstancias excepcionales donde están claramente marcados)

Tom Squires
fuente
Sí, estado mutable + evaluación perezosa = muerte. Creo que los únicos puntos que perdí en mi final de SICP fueron sobre usar set!en un intérprete de Scheme flojo. > :(
Tikhon Jelvis
3
"el código diferido y el estado pueden mezclarse mal": realmente depende de cómo implemente el estado. Si lo implementa utilizando variables mutables compartidas y depende del orden de evaluación para que su estado sea coherente, tiene razón.
Giorgio
14

La evaluación perezosa no siempre es mejor.

Los beneficios de rendimiento de la evaluación diferida pueden ser excelentes, pero no es difícil evitar la mayoría de las evaluaciones innecesarias en entornos ansiosos; seguramente la pereza lo hace fácil y completo, pero rara vez la evaluación innecesaria en el código es un problema importante.

Lo bueno de la evaluación diferida es cuando te permite escribir código más claro; obtener la décima prima filtrando una lista infinita de números naturales y tomando el décimo elemento de esa lista es una de las formas más concisas y claras de proceder: (pseudocódigo)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Creo que sería bastante difícil expresar las cosas de manera tan concisa sin pereza.

Pero la pereza no es la respuesta a todo. Para empezar, la pereza no se puede aplicar de forma transparente en presencia de estado, y creo que la apatía no se puede detectar automáticamente (a menos que esté trabajando, por ejemplo, Haskell, cuando el estado es bastante explícito). Por lo tanto, en la mayoría de los idiomas, la pereza debe hacerse manualmente, lo que hace que las cosas sean menos claras y, por lo tanto, elimina uno de los grandes beneficios de la evaluación diferida.

Además, la pereza tiene inconvenientes de rendimiento, ya que incurre en una sobrecarga significativa de mantener las expresiones no evaluadas; usan el almacenamiento y son más lentos para trabajar que los valores simples. No es raro descubrir que debe codificar con entusiasmo porque la versión perezosa es lenta y, a veces, es difícil razonar sobre el rendimiento.

Como suele suceder, no existe una mejor estrategia absoluta. Lazy es excelente si puede escribir un mejor código aprovechando las estructuras de datos infinitas u otras estrategias que le permite usar, pero ansioso puede ser más fácil de optimizar.

alex
fuente
¿Sería posible que un compilador realmente inteligente mitigue significativamente la sobrecarga? o incluso aprovechar la pereza para optimizaciones adicionales?
Tikhon Jelvis
3

Aquí hay una breve comparación de los pros y los contras de una evaluación ansiosa y perezosa:

  • Evaluación ansiosa:

    • Sobrecarga potencial de evaluar cosas innecesariamente.

    • Sin trabas, evaluación rápida.

  • Evaluación perezosa:

    • No hay evaluación innecesaria.

    • Gastos generales de contabilidad en cada uso de un valor.

Entonces, si tiene muchas expresiones que nunca tienen que ser evaluadas, lazy es mejor; sin embargo, si nunca tiene una expresión que no necesita ser evaluada, lazy es pura sobrecarga.

Ahora, echemos un vistazo al software del mundo real: ¿cuántas de las funciones que escribe no requieren la evaluación de todos sus argumentos? Especialmente con las funciones cortas modernas que solo hacen una cosa, el porcentaje de funciones que entran en esta categoría es muy bajo. Por lo tanto, la evaluación perezosa solo introduciría la sobrecarga de contabilidad la mayor parte del tiempo, sin la posibilidad de guardar realmente nada.

En consecuencia, la evaluación perezosa simplemente no paga en promedio, la evaluación entusiasta es la mejor opción para el código moderno.

cmaster
fuente
1
"Gastos generales de contabilidad en cada uso de un valor": no creo que los gastos generales de contabilidad sean mayores que, por ejemplo, verificar referencias nulas en un lenguaje como Java. En ambos casos, debe verificar un bit de información (evaluado / pendiente versus nulo / no nulo) y debe hacerlo cada vez que use un valor. Entonces, sí, hay una sobrecarga, pero es mínima.
Giorgio
1
"¿Cuántas de las funciones que escribe no requieren la evaluación de todos sus argumentos?": Esta es solo una aplicación de ejemplo. ¿Qué pasa con las estructuras recursivas de datos infinitos? ¿Se pueden implementar con una evaluación entusiasta? Puede usar iteradores, pero la solución no siempre es tan concisa. Por supuesto, probablemente no te pierdas algo que nunca has tenido la oportunidad de usar ampliamente.
Giorgio
2
"En consecuencia, la evaluación perezosa simplemente no paga en promedio, la evaluación entusiasta es la mejor opción para el código moderno".
Giorgio
1
@Giorgio Es posible que la sobrecarga no le parezca mucho, pero los condicionales son una de las cosas que las CPU modernas apestan: una rama errónea suele forzar una descarga completa de la tubería, desechando el trabajo de más de diez ciclos de CPU. No quieres condiciones innecesarias en tu circuito interno. Pagar diez ciclos adicionales por argumento de función es casi tan inaceptable para el código sensible al rendimiento como codificarlo en Java. Tiene razón en que la evaluación perezosa le permite realizar algunos trucos que no puede hacer fácilmente con una evaluación entusiasta. Pero la gran mayoría del código no necesita estos trucos.
cmaster
2
Esto parece ser una respuesta por inexperiencia con idiomas con evaluación perezosa. Por ejemplo, ¿qué pasa con las estructuras de datos infinitos?
Andres F.
3

Como señaló @DeadMG, la evaluación diferida requiere gastos generales de contabilidad. Esto puede ser costoso en relación con la evaluación entusiasta. Considere esta afirmación:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Esto tomará un poco de cálculo para calcular. Si uso una evaluación diferida, entonces debo verificar si se ha evaluado cada vez que la uso. Si esto está dentro de un circuito cerrado muy utilizado, la sobrecarga aumenta significativamente, pero no hay beneficio.

Con una evaluación entusiasta y un compilador decente, la fórmula se calcula en tiempo de compilación. La mayoría de los optimizadores moverán la asignación de cualquier bucle en el que ocurra, si corresponde.

La evaluación diferida es la más adecuada para cargar datos a los que se accede con poca frecuencia y tiene una alta sobrecarga para recuperar. Por lo tanto, es más apropiado para casos extremos que la funcionalidad principal.

En general, es una buena práctica evaluar cosas a las que se accede con frecuencia lo antes posible. La evaluación perezosa no funciona con esta práctica. Si siempre tendrá acceso a algo, lo único que hará una evaluación perezosa es agregar gastos generales. El costo / beneficio del uso de la evaluación diferida disminuye a medida que es menos probable que se acceda al elemento al que se accede.

Usar siempre una evaluación diferida también implica una optimización temprana. Esta es una mala práctica que a menudo da como resultado un código que es mucho más complejo y costoso que de lo contrario podría ser el caso. Desafortunadamente, la optimización prematura a menudo da como resultado un código que funciona más lentamente que un código más simple. Hasta que pueda medir el efecto de la optimización, es una mala idea optimizar su código.

Evitar la optimización prematura no entra en conflicto con las buenas prácticas de codificación. Si no se aplicaron las buenas prácticas, las optimizaciones iniciales pueden consistir en aplicar buenas prácticas de codificación, como mover cálculos fuera de los bucles.

BillThor
fuente
1
Pareces estar discutiendo por inexperiencia. Le sugiero que lea el documento "Por qué es importante la programación funcional" de Wadler. Dedica una sección importante que explica el por qué de la evaluación diferida (pista: tiene poco que ver con el rendimiento, la optimización temprana o la "carga de datos a los que se accede con poca frecuencia", y todo que ver con la modularidad).
Andres F.
@AndresF He leído el documento al que te refieres. Estoy de acuerdo con el uso de la evaluación perezosa en tales casos. La evaluación temprana puede no ser apropiada, pero diría que devolver el subárbol para el movimiento seleccionado puede tener un beneficio significativo si se pueden agregar movimientos adicionales fácilmente. Sin embargo, desarrollar esa funcionalidad podría ser una optimización prematura. Fuera de la programación funcional, tengo problemas importantes con el uso de la evaluación diferida y la falla en el uso de la evaluación diferida. Hay informes de costos de rendimiento significativos como resultado de una evaluación diferida en la programación funcional.
BillThor
2
¿Como? Hay informes de costos de rendimiento significativos cuando se usa también una evaluación entusiasta (costos en forma de evaluación innecesaria, así como la no terminación del programa). Hay costos para casi cualquier otra característica (mal utilizada), ahora que lo pienso. La modularidad en sí misma puede tener un costo; El problema es si vale la pena.
Andres F.
3

Si potencialmente tenemos que evaluar completamente una expresión para determinar su valor, la evaluación diferida puede ser una desventaja. Digamos que tenemos una larga lista de valores booleanos y queremos saber si todos son verdaderos:

[True, True, True, ... False]

Para hacer esto, tenemos que mirar cada elemento de la lista, sin importar qué, así que no hay posibilidad de cortar perezosamente la evaluación. Podemos usar un pliegue para determinar si todos los valores booleanos en la lista son verdaderos. Si usamos un pliegue a la derecha, que usa la evaluación diferida, no obtenemos ninguno de los beneficios de la evaluación diferida porque tenemos que mirar cada elemento de la lista:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Un pliegue a la derecha será mucho más lento en este caso que un pliegue estricto a la izquierda, que no utiliza una evaluación perezosa:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

La razón es que un pliegue estricto hacia la izquierda utiliza la recursividad de cola, lo que significa que acumula el valor de retorno y no se acumula y almacena en la memoria una gran cadena de operaciones. Esto es mucho más rápido que el plegado lento a la derecha porque ambas funciones tienen que mirar la lista completa de todos modos y el plegado a la derecha no puede usar la recursión de cola. Entonces, el punto es que debes usar lo que sea mejor para la tarea en cuestión.

tail_recursion
fuente
"Entonces, el punto es que debes usar lo que sea mejor para la tarea en cuestión". +1
Giorgio