En la teoría de lenguajes de programación , la evaluación perezosa , o llamada por necesidad , [1] es una estrategia de evaluación que retrasa la evaluación de una expresión hasta que se necesita su valor ( evaluación no estricta ) y que también evita evaluaciones repetidas (mediante el uso de la compartición ). [2] [3]
Los beneficios de la evaluación perezosa incluyen:
La evaluación perezosa a menudo se combina con la memorización , como se describe en Writing Efficient Programs de Jon Bentley . [4] Después de calcular el valor de una función para ese parámetro o conjunto de parámetros, el resultado se almacena en una tabla de búsqueda que está indexada por los valores de esos parámetros; la próxima vez que se llama a la función, se consulta la tabla para determinar si el resultado para esa combinación de valores de parámetros ya está disponible. Si es así, simplemente se devuelve el resultado almacenado. Si no, se evalúa la función y se agrega otra entrada a la tabla de búsqueda para su reutilización.
La evaluación perezosa es difícil de combinar con características imperativas como el manejo de excepciones y la entrada/salida , porque el orden de las operaciones se vuelve indeterminado.
El opuesto de la evaluación perezosa es la evaluación ansiosa , a veces conocida como evaluación estricta. La evaluación ansiosa es la estrategia de evaluación empleada en la mayoría de los lenguajes de programación [ cuantitativos ] .
La evaluación perezosa fue introducida para el cálculo lambda por Christopher Wadsworth [5] y empleada por el Plessey System 250 como una parte crítica de una metamáquina de cálculo lambda, reduciendo la sobrecarga de resolución para el acceso a objetos en un espacio de direcciones de capacidad limitada . [6] Para los lenguajes de programación, fue introducida de forma independiente por Peter Henderson y James H. Morris [7] y por Daniel P. Friedman y David S. Wise. [8] [9]
La evaluación retrasada se utiliza particularmente en lenguajes de programación funcional . Cuando se utiliza la evaluación retrasada, una expresión no se evalúa tan pronto como se vincula a una variable, sino cuando el evaluador se ve obligado a producir el valor de la expresión. Es decir, una declaración como x = expression;
(es decir, la asignación del resultado de una expresión a una variable) claramente exige que se evalúe la expresión y que el resultado se coloque en x
, pero lo que realmente está en x
es irrelevante hasta que haya una necesidad de su valor a través de una referencia a x
en alguna expresión posterior cuya evaluación podría diferirse, aunque eventualmente el árbol de dependencias de rápido crecimiento se podaría para producir algún símbolo en lugar de otro para que el mundo exterior lo vea. [10]
La evaluación diferida permite definir estructuras de control de forma normal, y no como primitivas o técnicas de tiempo de compilación. Por ejemplo, se pueden definir operadores de evaluación if-then-else y de cortocircuito : [11] [12]
siEntoncesDeLoContrario Verdadero b c = b siEntoncesDeLoContrario Falso b c = c -- o Verdadero || b = Verdadero Falso || b = b -- y Verdadero && b = b Falso && b = Falso
Estos tienen la semántica habitual, es decir, evalúa (a), entonces si y solo si (a) se evalúa como verdadero, evalúa (b); de lo contrario, evalúa (c). Es decir, se evaluará exactamente uno de (b) o (c). De manera similar, para , si la parte fácil da Verdadero, se podría evitar la gran cantidad de trabajo de expresión. Finalmente, al evaluar , si SafeToTry es falso, no se intentará evaluar la Expresión .ifThenElse a b c
EasilyComputed || LotsOfWork
SafeToTry && Expression
Por el contrario, en un lenguaje ansioso, la definición anterior para evaluaría (a), (b) y (c) independientemente del valor de (a). Este no es el comportamiento deseado, ya que (b) o (c) pueden tener efectos secundarios , tardar mucho tiempo en calcularse o generar errores. Por lo general, es posible introducir estructuras de control perezosas definidas por el usuario en lenguajes ansiosos como funciones, aunque pueden alejarse de la sintaxis del lenguaje para la evaluación ansiosa: A menudo, los cuerpos de código involucrados deben estar envueltos en un valor de función, de modo que se ejecuten solo cuando se los llama.ifThenElse a b c
La evaluación retrasada tiene la ventaja de poder crear listas infinitas calculables sin bucles infinitos ni cuestiones de tamaño que interfieran en el cálculo. Los valores reales solo se calculan cuando son necesarios. Por ejemplo, se podría crear una función que cree una lista infinita (a menudo llamada flujo ) de números de Fibonacci . El cálculo del n -ésimo número de Fibonacci sería simplemente la extracción de ese elemento de la lista infinita, lo que obligaría a la evaluación de solo los primeros n miembros de la lista. [13] [14]
Tomemos como ejemplo este programa trivial en Haskell :
numberFromInfiniteList :: Int -> Int numberFromInfiniteList n = infinito !! n - 1 donde infinito = [ 1 .. ] principal = imprimir $ numeroDeListaInfinita 4
En la función numberFromInfiniteList , el valor de infinito es un rango infinito, pero hasta que se necesita un valor real (o más específicamente, un valor específico en un índice determinado), la lista no se evalúa, e incluso entonces, solo se evalúa según sea necesario (es decir, hasta el índice deseado). Siempre que el programador sea cuidadoso, el programa se completa normalmente. Sin embargo, ciertos cálculos pueden dar como resultado que el programa intente evaluar un número infinito de elementos; por ejemplo, solicitar la longitud de la lista o intentar sumar los elementos de la lista con una operación de plegado daría como resultado que el programa no pudiera terminar o se quedara sin memoria .
Como otro ejemplo, la lista de todos los números de Fibonacci se puede escribir en el lenguaje de programación Haskell como: [14]
fibs = 0 : 1 : zipWith ( + ) fibs ( cola fibs )
En la sintaxis de Haskell, " :
" antepone un elemento a una lista, tail
devuelve una lista sin su primer elemento y zipWith
utiliza una función especificada (en este caso, la adición) para combinar los elementos correspondientes de dos listas para producir una tercera. [13]
En los sistemas de ventanas de ordenador , la representación de la información en la pantalla se realiza mediante eventos de exposición que activan el código de visualización en el último momento posible. De esta manera, los sistemas de ventanas evitan realizar actualizaciones innecesarias del contenido de la pantalla. [15]
Otro ejemplo de pereza en los sistemas informáticos modernos es la asignación de páginas mediante copia en escritura o paginación por demanda , donde la memoria se asigna solo cuando se modifica un valor almacenado en esa memoria. [15]
La pereza puede ser útil en situaciones de alto rendimiento. Un ejemplo es la función mmap de Unix, que permite cargar páginas desde el disco en función de la demanda , de modo que solo se carguen en la memoria las páginas que se utilizan realmente y no se asigne memoria innecesaria.
MATLAB implementa la copia al editar , donde las matrices que se copian tienen su almacenamiento de memoria real replicado solo cuando se modifica su contenido, lo que posiblemente lleve a un error de falta de memoria al actualizar un elemento posteriormente en lugar de durante la operación de copia. [16]
El número de reducciones beta para reducir un término lambda con llamada por necesidad no es mayor que el número necesario para la reducción de llamada por valor o llamada por nombre . [17] [18] Y con ciertos programas el número de pasos puede ser mucho menor, por ejemplo, una familia específica de términos lambda que usan numerales de Church toman una cantidad infinita de pasos con llamada por valor (es decir, nunca se completan), un número exponencial de pasos con llamada por nombre, pero solo un número polinomial con llamada por necesidad. La llamada por necesidad incorpora dos optimizaciones: nunca repetir el trabajo (similar a la llamada por valor) y nunca realizar trabajo innecesario (similar a la llamada por nombre). [19] La evaluación perezosa también puede conducir a una reducción en la huella de memoria , ya que los valores se crean cuando son necesarios. [20]
En la práctica, la evaluación diferida puede causar problemas de rendimiento significativos en comparación con la evaluación ansiosa. Por ejemplo, en las arquitecturas informáticas modernas, retrasar un cálculo y realizarlo más tarde es más lento que realizarlo inmediatamente. Esto se puede aliviar mediante un análisis de estrictez . [19] La evaluación diferida también puede introducir fugas de memoria debido a expresiones no evaluadas. [21] [22]
Algunos lenguajes de programación retrasan la evaluación de expresiones por defecto, y otros proporcionan funciones o sintaxis especial para retrasar la evaluación. En Miranda y Haskell , la evaluación de los argumentos de función se retrasa por defecto. En muchos otros lenguajes, la evaluación se puede retrasar suspendiendo explícitamente el cálculo utilizando una sintaxis especial (como con " " y " " de Scheme y " " y " " de OCaml ) o, de forma más general, envolviendo la expresión en un thunk . El objeto que representa dicha evaluación explícitamente retrasada se denomina futuro perezoso . Raku utiliza la evaluación perezosa de listas, por lo que se pueden asignar listas infinitas a las variables y utilizarlas como argumentos para funciones, pero a diferencia de Haskell y Miranda, Raku no utiliza la evaluación perezosa de operadores aritméticos y funciones por defecto. [10]delay
force
lazy
Lazy.force
En lenguajes de programación perezosos como Haskell, aunque el valor predeterminado es evaluar expresiones solo cuando se las solicita, en algunos casos es posible hacer que el código sea más ansioso (o, por el contrario, hacerlo más perezoso nuevamente después de que se lo haya hecho más ansioso). Esto se puede hacer codificando explícitamente algo que fuerce la evaluación (lo que puede hacer que el código sea más ansioso) o evitando ese código (lo que puede hacer que el código sea más perezoso). La evaluación estricta generalmente implica entusiasmo, pero técnicamente son conceptos diferentes.
Sin embargo, hay una optimización implementada en algunos compiladores llamada análisis de estrictez , que, en algunos casos, permite al compilador inferir que siempre se utilizará un valor. En tales casos, esto puede hacer que la elección del programador de forzar o no ese valor en particular sea irrelevante, porque el análisis de estrictez forzará una evaluación estricta .
En Haskell, marcar los campos del constructor como estrictos significa que sus valores siempre se exigirán de inmediato. La seq
función también se puede utilizar para exigir un valor inmediatamente y luego pasarlo, lo que resulta útil si un campo del constructor debe ser generalmente perezoso. Sin embargo, ninguna de estas técnicas implementa la estrictez recursivadeepSeq
; para eso, se inventó una función llamada .
Además, la coincidencia de patrones en Haskell 98 es estricta por defecto, por lo que ~
se debe usar el calificador para que sea perezosa. [23]
En Java , la evaluación diferida se puede realizar mediante el uso de objetos que tienen un método para evaluarlos cuando se necesita el valor. El cuerpo de este método debe contener el código necesario para realizar esta evaluación. Desde la introducción de las expresiones lambda en Java SE8, Java ha admitido una notación compacta para esto. La siguiente interfaz genérica de ejemplo proporciona un marco para la evaluación diferida: [24] [25]
interfaz Lazy < T > { T eval (); }
La Lazy
interfaz con su eval()
método es equivalente a la Supplier
interfaz con su get()
método en la java.util.function
biblioteca. [26] [27] : 200
Cada clase que implementa la Lazy
interfaz debe proporcionar un eval
método, y las instancias de la clase pueden contener los valores que el método necesite para realizar una evaluación diferida. Por ejemplo, considere el siguiente código para calcular e imprimir de manera diferida 2 10 :
Lazy < Entero > a = () -> 1 ; para ( int i = 0 ; i < 10 ; i ++ ) { Lazy < Entero > b = a ; a = () -> b . eval () + b . eval ( ); } System . println ( "a = " + a . eval ()) ;
En el ejemplo anterior, la variable a inicialmente hace referencia a un objeto entero perezoso creado por la expresión lambda . Evaluar esta expresión lambda es similar [a] a construir una nueva instancia de una clase anónima que se implementa con un método eval que devuelve 1 .() -> 1
Lazy<Integer>
Cada iteración del bucle vincula a a un nuevo objeto creado mediante la evaluación de la expresión lambda dentro del bucle. Cada uno de estos objetos contiene una referencia a otro objeto perezoso, b , y tiene un método eval que lo invoca dos veces y devuelve la suma. La variable b es necesaria aquí para cumplir con el requisito de Java de que las variables a las que se hace referencia desde dentro de una expresión lambda sean efectivamente finales.b.eval()
Este es un programa ineficiente porque esta implementación de enteros perezosos no memoriza el resultado de llamadas anteriores a eval . También implica un considerable autoboxing y unboxing . Lo que puede no ser obvio es que, al final del bucle, el programa ha construido una lista enlazada de 11 objetos y que todas las adiciones reales involucradas en el cálculo del resultado se realizan en respuesta a la llamada a en la línea final del código. Esta llamada recorre recursivamente la lista para realizar las adiciones necesarias.a.eval()
Podemos construir una clase Java que memorice un objeto perezoso de la siguiente manera: [24] [25]
clase Memo < T > implementa Lazy < T > { private Lazy < T > lazy ; // una expresión lazy, eval la establece en nula private T memo ; // el memorando del valor anterior public Memo ( Lazy < T > lazy ) { this.lazy = lazy ; } público T eval () { si ( perezoso ! = null ) { memo = perezoso . eval (); perezoso = null ; } devolver memo ; } }
Esto permite reescribir el ejemplo anterior para que sea mucho más eficiente. Mientras que el original se ejecutaba en un tiempo exponencial en cuanto a la cantidad de iteraciones, la versión memorizada se ejecuta en un tiempo lineal :
Lazy < Integer > a = () -> 1 ; para ( int i = 0 ; i < 10 ; i ++ ) { Lazy < Integer > b = a ; a = nuevo Memo < Integer > (() -> b . eval () + b . eval ( )); } System . println ( " a = " + a . eval ());
Las expresiones lambda de Java son simplemente azúcar sintáctica . Todo lo que se pueda escribir con una expresión lambda se puede reescribir como una llamada para construir una instancia de una clase interna anónima que implemente la interfaz, [a] y cualquier uso de una clase interna anónima se puede reescribir utilizando una clase interna con nombre, y cualquier clase interna con nombre se puede mover al nivel de anidamiento más externo.
En JavaScript , la evaluación diferida se puede simular mediante un generador . Por ejemplo, el flujo de todos los números de Fibonacci se puede escribir, mediante memorización , como:
/** * Las funciones generadoras devuelven objetos generadores, que concretan la evaluación diferida. * @return {!Generator<bigint>} Un generador no nulo de números enteros. */ function * fibonacciNumbers () { let memo = [ 1n , - 1n ]; // crea el estado inicial (por ejemplo, un vector de números "negafibonacci") while ( true ) { // repite indefinidamente memo = [ memo [ 0 ] + memo [ 1 ], memo [ 0 ]]; // actualiza el estado en cada evaluación yield memo [ 0 ]; // produce el siguiente valor y suspende la ejecución hasta que se reanude } } let stream = fibonacciNumbers (); // crea un flujo de números evaluado de forma diferida let first10 = Array . from ( new Array ( 10 ), () => stream . next (). value ); // evalúa solo los primeros 10 números console . log ( first10 ); // la salida es [0n, 1n, 1n, 2n, 3n, 5n, 8n, 13n, 21n, 34n]
En Python 2.x, la range()
función [28] calcula una lista de números enteros. La lista completa se almacena en la memoria cuando se evalúa la primera instrucción de asignación, por lo que este es un ejemplo de evaluación inmediata o ansiosa:
>>> r = rango ( 10 ) >>> imprimir r [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> imprimir r [ 3 ] 3
En Python 3.x, la range()
función [29] devuelve un generador que calcula los elementos de la lista a pedido. Los elementos solo se generan cuando son necesarios (por ejemplo, cuando print(r[3])
se evalúa en el siguiente ejemplo), por lo que este es un ejemplo de evaluación diferida o diferida:
>>> r = rango ( 10 ) >>> imprimir ( r ) rango(0, 10) >>> imprimir ( r [ 3 ]) 3
En Python 2.x es posible utilizar una función llamada xrange()
que devuelve un objeto que genera los números del rango según la demanda. La ventaja de esto xrange
es que el objeto generado siempre ocupará la misma cantidad de memoria.
>>> r = xrange ( 10 ) >>> print ( r ) xrange(10) >>> lst = [ x para x en r ] >>> print ( lst ) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
A partir de la versión 2.2, Python manifiesta la evaluación diferida mediante la implementación de iteradores (secuencias diferidas), a diferencia de las secuencias de tuplas o listas. Por ejemplo (Python 2):
>>> números = rango ( 10 ) >>> iterador = iter ( números ) >>> imprimir números [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> imprimir iterador <listiterator object at 0xf7e8dd4c> >>> imprimir iterador . next () 0
En el marco .NET , es posible realizar una evaluación diferida utilizando la clase . [30] La clase se puede explotar fácilmente en F# utilizando la palabra clave , mientras que el método forzará la evaluación. También existen colecciones especializadas como esa que proporciona soporte integrado para la evaluación diferida. System.Lazy<T>
lazy
force
Microsoft.FSharp.Collections.Seq
sea fibonacci = Seq . unfold ( fun ( x , y ) -> Some ( x , ( y , x + y ))) ( 0I , 1I ) fibonacci |> Seq . nth 1000
En C# y VB.NET, la clase se utiliza directamente. System.Lazy<T>
public int Suma () { int a = 0 ; int b = 0 ; Lazy < int > x = new Lazy < int > (() => a + b ); a = 3 ; b = 5 ; devuelve x . Valor ; // devuelve 8 }
O con un ejemplo más práctico:
// cálculo recursivo del n'ésimo número de Fibonacci public int Fib ( int n ) { return ( n == 1 ) ? 1 : ( n == 2 ) ? 1 : Fib ( n - 1 ) + Fib ( n - 2 ); } public void Main () { Console.WriteLine ( "¿Qué número de Fibonacci desea calcular?" ) ; int n = Int32.Parse ( Console.ReadLine ( )); Lazy < int > fib = new Lazy < int > ( ( ) => Fib ( n ) ); // la función se prepara, pero no se ejecuta boolexecute ; if ( n > 100 ) { Console.WriteLine ( " Esto puede llevar algún tiempo. ¿Realmente desea calcular este gran número? [y/n]" ) ; execute = ( Console.ReadLine ( ) == " y " ) ; } elseexecute = true ; if ( execute ) Console.WriteLine ( fib.Value ) ; // el número solo se calcula si es necesario }
Otra forma es utilizar la yield
palabra clave:
// evaluación ansiosa public IEnumerable < int > Fibonacci ( int x ) { IList < int > fibs = new List < int > (); int prev = - 1 ; int next = 1 ; para ( int i = 0 ; i < x ; i ++ ) { int suma = prev + next ; prev = next ; next = suma ; fibs . Add ( suma ); } return fibs ; } // evaluación perezosa public IEnumerable < int > LazyFibonacci ( int x ) { int prev = - 1 ; int next = 1 ; for ( int i = 0 ; i < x ; i ++ ) { int sum = prev + next ; prev = next ; next = sum ; yield return sum ; } }