En un lenguaje de programación , una estrategia de evaluación es un conjunto de reglas para evaluar expresiones. [1] El término se usa a menudo para referirse a la noción más específica de una estrategia de paso de parámetros [2] que define el tipo de valor que se pasa a la función para cada parámetro (la estrategia de enlace ) [3] y si evaluar los parámetros de una llamada a función y, de ser así, en qué orden (el orden de evaluación ). [4] La noción de estrategia de reducción es distinta, [5] aunque algunos autores combinan los dos términos y la definición de cada término no existe un consenso generalizado. [6]
Para ilustrar, ejecutar una llamada de función f(a,b)
puede primero evaluar los argumentos a
y b
almacenar los resultados en referencias o ubicaciones de memoria ref_a
y ref_b
luego evaluar el cuerpo de la función con esas referencias pasadas. Esto le da a la función la capacidad de buscar los valores de los argumentos originales pasados. mediante la desreferenciación de los parámetros (algunos lenguajes usan operadores específicos para realizar esto), modificarlos mediante asignación como si fueran variables locales y devolver valores a través de las referencias. Esta es la estrategia de evaluación de llamada por referencia. [7]
La estrategia de evaluación es parte de la semántica de la definición del lenguaje de programación. Algunos lenguajes, como PureScript , tienen variantes con diferentes estrategias de evaluación. Algunos lenguajes declarativos , como Datalog , admiten múltiples estrategias de evaluación. Algunos idiomas definen una convención de llamada . [ se necesita aclaración ]
Esta es una tabla de estrategias de evaluación y idiomas representativos por año de introducción. Los idiomas representativos se enumeran en orden cronológico, comenzando con el idioma que introdujo la estrategia y seguido por los idiomas destacados que utilizan la estrategia. [8] : 434
Mientras que el orden de las operaciones define el árbol de sintaxis abstracta de la expresión, el orden de evaluación define el orden en el que se evalúan las expresiones. Por ejemplo, el programa Python
def f ( x ): imprimir ( x , fin = '' ) devolver ximprimir ( f ( 1 ) + f ( 2 ), fin = '' )
resultados 123
debido al orden de evaluación de izquierda a derecha de Python, pero un programa similar en OCaml :
let f x = print_int x ; X ;; print_int ( f 1 + f 2 )
salidas 213
debido al orden de evaluación de derecha a izquierda de OCaml.
El orden de evaluación es principalmente visible en el código con efectos secundarios , pero también afecta el rendimiento del código porque un orden rígido inhibe la programación de instrucciones . Por esta razón, los estándares de lenguajes como C++ tradicionalmente dejaban el orden sin especificar, aunque lenguajes como Java y C# definen el orden de evaluación de izquierda a derecha [8] : 240–241 y el estándar C++17 ha agregado restricciones en el orden de evaluación. orden de evaluación. [21]
El orden aplicativo es una familia de órdenes de evaluación en la que los argumentos de una función se evalúan completamente antes de aplicar la función. [22] Esto tiene el efecto de hacer que la función sea estricta , es decir, el resultado de la función no está definido si alguno de los argumentos no está definido, por lo que la evaluación del orden aplicativo se denomina más comúnmente evaluación estricta . Además, una llamada a una función se realiza tan pronto como se encuentra en un procedimiento, por lo que también se denomina evaluación ansiosa o evaluación codiciosa . [23] [24] Algunos autores se refieren a la evaluación estricta como "llamada por valor" debido a que la estrategia de vinculación de llamada por valor requiere una evaluación estricta. [4]
Common Lisp, Eiffel y Java evalúan los argumentos de las funciones de izquierda a derecha. C deja el orden indefinido. [25] El esquema requiere que el orden de ejecución sea la ejecución secuencial de una permutación no especificada de los argumentos. [26] OCaml igualmente deja el orden sin especificar, pero en la práctica evalúa los argumentos de derecha a izquierda debido al diseño de su máquina abstracta . [27] Todas estas son evaluaciones estrictas.
Un orden de evaluación no estricto es un orden de evaluación que no es estricto, es decir, una función puede devolver un resultado antes de que todos sus argumentos se evalúen por completo. [28] : 46–47 El ejemplo prototípico es la evaluación de orden normal , que no evalúa ninguno de los argumentos hasta que son necesarios en el cuerpo de la función. [29] La evaluación de orden normal tiene la propiedad de que termina sin error siempre que cualquier otra orden de evaluación hubiera terminado sin error. [30] El nombre "orden normal" proviene del cálculo lambda, donde la reducción de orden normal encontrará una forma normal si la hay (es una estrategia de reducción "normalizante" ). [31] La evaluación diferida se clasifica en este artículo como una técnica vinculante en lugar de un orden de evaluación. Pero esta distinción no siempre se sigue y algunos autores definen la evaluación perezosa como evaluación de orden normal o viceversa, [22] [32] o confunden la no rigurosidad con la evaluación perezosa. [28] : 43–44
Las expresiones booleanas en muchos idiomas utilizan una forma de evaluación no estricta llamada evaluación de cortocircuito , donde la evaluación evalúa la expresión de la izquierda pero puede omitir la expresión de la derecha si se puede determinar el resultado; por ejemplo, en una expresión disyuntiva (OR) donde true
está encontrado, o en una expresión conjuntiva (Y) donde false
se encuentra, y así sucesivamente. [32] Las expresiones condicionales utilizan de manera similar una evaluación no estricta: solo se evalúa una de las ramas. [28]
Con la evaluación de orden normal, las expresiones que contienen un cálculo costoso, un error o un bucle infinito se ignorarán si no son necesarias, [4] permitiendo la especificación de construcciones de flujo de control definidas por el usuario, una función no disponible con la evaluación de orden aplicativa. La evaluación de orden normal utiliza estructuras complejas, como procesadores para expresiones no evaluadas, en comparación con la pila de llamadas utilizada en la evaluación de orden aplicativa. [33] Históricamente, la evaluación de órdenes normales ha carecido de herramientas de depuración utilizables debido a su complejidad. [34]
Al llamar por valor (o pasar por valor), el valor evaluado de la expresión del argumento se vincula a la variable correspondiente en la función (frecuentemente copiando el valor en una nueva región de memoria). Si la función o procedimiento puede asignar valores a sus parámetros, solo se asigna su variable local; es decir, todo lo que se pasa a una llamada de función no cambia en el alcance de la persona que llama cuando la función regresa. Por ejemplo, en Pascal , pasar una matriz por valor hará que se copie toda la matriz y cualquier mutación en esta matriz será invisible para la persona que llama: [35]
programa principal ; utiliza crt ; procedimiento PrintArray ( a : Matriz de números enteros ) ; var i : Entero ; comenzar para i : = Bajo ( a ) a Alto ( a ) Escribir ( a [ i ] ) ; EscribirLn () ; fin ; Modificar procedimiento ( fila : matriz de números enteros ) ; comenzar PrintArray ( fila ) ; // 123 Fila [ 1 ] := 4 ; PrintArray ( fila ) ; // 143 fin ; Var A : Matriz de números enteros ; comenzar A : = [ 1 , 2 , 3 ] ; Imprimirmatriz ( A ) ; // 123 Modificar ( A ) ; Imprimirmatriz ( A ) ; // 123 fin .
Estrictamente hablando, bajo llamada por valor, ninguna operación realizada por la rutina llamada puede ser visible para quien llama, excepto como parte del valor de retorno. [16] Esto implica una forma de programación puramente funcional en la semántica de implementación. Sin embargo, el circunloquio "llamar por valor donde el valor es una referencia" se ha vuelto común, por ejemplo, en la comunidad Java. [36] En comparación con el paso por valor tradicional, el valor que se pasa no es un valor tal como se entiende en el significado ordinario de valor, como un número entero que puede escribirse como un literal, sino un identificador de referencia interno de la implementación . Las mutaciones en este identificador de referencia son visibles en la persona que llama. Debido a la mutación visible, esta forma de "llamada por valor" se denomina más propiamente llamada por compartir. [dieciséis]
En lenguajes puramente funcionales , los valores y las estructuras de datos son inmutables, por lo que una función no tiene posibilidad de modificar ninguno de sus argumentos. Como tal, normalmente no existe una diferencia semántica entre pasar por valor y pasar por referencia o un puntero a la estructura de datos, y las implementaciones frecuentemente utilizan llamada por referencia internamente para obtener beneficios de eficiencia. No obstante, estos lenguajes generalmente se describen como lenguajes de llamada por valor.
Llamar por referencia (o pasar por referencia) es una estrategia de evaluación en la que un parámetro está vinculado a una referencia implícita a la variable utilizada como argumento, en lugar de una copia de su valor. Normalmente, esto significa que la función puede modificar (es decir, asignar a ) la variable utilizada como argumento, algo que será visto por quien la llama. Por lo tanto, la llamada por referencia se puede utilizar para proporcionar un canal adicional de comunicación entre la función llamada y la función que llama. Pasar por referencia puede mejorar significativamente el rendimiento: llamar a una función con una estructura de muchos megabytes como argumento no tiene que copiar la estructura grande, solo la referencia a la estructura (que generalmente es una palabra de máquina y solo unos pocos bytes). Sin embargo, un lenguaje de llamada por referencia hace que sea más difícil para un programador rastrear los efectos de una llamada a función y puede introducir errores sutiles.
Debido a la variación en la sintaxis, la diferencia entre llamada por referencia (donde el tipo de referencia es implícito) y llamada por uso compartido (donde el tipo de referencia es explícito) a menudo no está clara a primera vista. Una prueba de fuego simple es si es posible escribir una swap(a, b)
función tradicional en el idioma. [36] Por ejemplo en Fortran:
programa Principal implícito ninguno entero :: a = 1 entero :: b = 2 llamada Intercambiar ( a , b ) imprimir * , a , b ! 2 1 contiene subrutina Intercambio ( a , b ) entero , intención ( inout ) :: a , b entero :: temp temp = a a = b b = temp fin de subrutina Intercambio fin del programa Principal
Por lo tanto, la intención de Fortran inout
implementa la llamada por referencia; cualquier variable se puede convertir implícitamente en un identificador de referencia. Por el contrario, lo más cercano que se puede llegar en Java es:
clase principal { cuadro de clase estática { valor int ; Caja pública ( valor int ) { this . valor = valor ; } } intercambio de vacío estático ( Caja a , Caja b ) { int temp = a . valor ; a . valor = b . valor ; b . valor = temperatura ; } public static void main ( String [] args ) { Cuadro a = nuevo Cuadro ( 1 ); Cuadro b = nuevo Cuadro ( 2 ); intercambiar ( a , b ); Sistema . afuera . println ( String . formato ( "%d %d" , a . valor , b . valor )); } } // salida: 2 1
Box
donde se debe utilizar un tipo explícito para introducir un identificador. Java es llamada por uso compartido pero no llamada por referencia. [36]
La llamada por copia-restauración, también conocida como "copia de entrada y salida", "llamada por resultado de valor", "llamada por retorno de valor" (como se denomina en la comunidad de Fortran ), es una variación de la llamada por referencia. Con la llamada mediante copia-restauración, el contenido del argumento se copia a una nueva variable local a la invocación de la llamada. Luego, la función puede modificar esta variable, de manera similar a la llamada por referencia, pero como la variable es local, las modificaciones no son visibles fuera de la invocación de la llamada durante la llamada. Cuando regresa la llamada a la función, el contenido actualizado de esta variable se copia nuevamente para sobrescribir el argumento original ("restaurado"). [37]
La semántica de la llamada mediante copia-restauración es similar en muchos casos a la llamada por referencia, pero difiere cuando dos o más argumentos de función se superponen entre sí (es decir, apuntan a la misma variable en el entorno de la persona que llama). En la llamada por referencia, escribir en un argumento afectará al otro durante la ejecución de la función. Bajo una llamada mediante copia-restauración, escribir en un argumento no afectará al otro durante la ejecución de la función, pero al final de la llamada, los valores de los dos argumentos pueden diferir y no está claro qué argumento se copia primero y, por lo tanto, qué valor recibe la variable de la persona que llama. [38] Por ejemplo, Ada especifica que la asignación de copia para cada parámetro in out
o out
se produce en un orden arbitrario. [39] En el siguiente programa (ilegal en Ada 2012) [40] se puede ver que el comportamiento de GNAT es copiar en orden de izquierda a derecha al regresar:
con Ada.Text_IO ; utilizar Ada.Text_IO ;el procedimiento Test_Copy_Restore es el procedimiento Modificar ( A , B : in out Integer ) es comenzar A := A + 1 ; segundo := segundo + 2 ; finalizar Modificar ; X : Entero := 0 ; comenzar Modificar ( X , X ); Put_Line ( "X = " & Integer ' Imagen ( X )); finalizar Test_Copy_Restore ; -- $ gnatmake -gnatd.E test_copy_restore.adb; ./test_copy_restore -- test_copy_restore.adb:12:10: advertencia: el valor real grabable para "A" se superpone con el real para "B" [-gnatw.i] -- X = 2
Si el programa devolviera 1, estaría copiando de derecha a izquierda y, bajo llamada por semántica de referencia, el programa devolvería 3.
Cuando la referencia se pasa a la persona que llama sin inicializar (por ejemplo, un out
parámetro en Ada en lugar de un in out
parámetro), esta estrategia de evaluación puede denominarse "llamada por resultado".
Esta estrategia ha ganado atención en multiprocesamiento y llamadas a procedimientos remotos , [41] ya que, a diferencia de la llamada por referencia, no requiere comunicación frecuente entre subprocesos de ejecución para acceso a variables.
La llamada compartiendo (también conocida como "pasar compartiendo", "llamada por objeto" o "llamada compartiendo objeto") es una estrategia de evaluación intermedia entre la llamada por valor y la llamada por referencia. En lugar de que cada variable se exponga como referencia, sólo una clase específica de valores, denominada "referencias", " tipos en caja " u "objetos", tienen semántica de referencia, y son las direcciones de estos punteros las que se pasan a la función. . Al igual que la llamada por valor, el valor de la dirección pasada es una copia, y la asignación directa al parámetro de la función sobrescribe la copia y no es visible para la función que llama. Al igual que la llamada por referencia, la mutación del objetivo del puntero es visible para la función que llama. Las mutaciones de un objeto mutable dentro de la función son visibles para la persona que llama porque el objeto no se copia ni se clona; se comparte , de ahí el nombre "llamar compartiendo". [dieciséis]
La técnica fue notada por primera vez por Barbara Liskov en 1974 para el lenguaje CLU . [16] Es utilizado por muchos lenguajes modernos como Python (los valores compartidos se denominan "objetos"), [42] Java (objetos), Ruby (objetos), JavaScript (objetos), Scheme (estructuras de datos como vectores) , [43] AppleScript (listas, registros, fechas y objetos de script), OCaml y ML (referencias, registros, matrices, objetos y otros tipos de datos compuestos), Maple (rtables y tablas) y Tcl (objetos). [44] El término "llamar compartiendo" tal como se utiliza en este artículo no es de uso común; la terminología es inconsistente entre diferentes fuentes. Por ejemplo, en la comunidad Java, dicen que Java se llama por valor. [36]
Para objetos inmutables , no existe una diferencia real entre llamar por compartir y llamar por valor, excepto si la identidad del objeto es visible en el lenguaje. El uso de la llamada compartiendo con objetos mutables es una alternativa a los parámetros de entrada/salida : el parámetro no se asigna (el argumento no se sobrescribe y la identidad del objeto no se cambia), pero el objeto (argumento) se muta. [45]
Por ejemplo, en Python, las listas son mutables y se pasan mediante llamada mediante uso compartido, por lo que:
def f ( una_lista ): una_lista . añadir ( 1 )m = [] f ( m ) imprimir ( m )
produce [1]
porque el append
método modifica el objeto en el que se llama.
Por el contrario, las asignaciones dentro de una función no son perceptibles para quien llama. Por ejemplo, este código vincula el argumento formal a un nuevo objeto, pero no es visible para la persona que llama porque no muta a_list
:
def f ( una_lista ): una_lista = una_lista + [ 1 ] print ( una_lista ) # [1]m = [] f ( m ) imprimir ( m ) # []
Llamar por dirección , pasar por dirección o llamar/pasar por puntero es un método de paso de parámetros donde la dirección del argumento se pasa como parámetro formal. Dentro de la función, la dirección (puntero) se puede utilizar para acceder o modificar el valor del argumento. Por ejemplo, la operación de intercambio se puede implementar de la siguiente manera en C: [46]
#incluir <stdio.h> intercambio vacío ( int * a , int * b ) { int temp = * a ; * a = * b ; * b = temperatura ; } int principal () { int a = 1 ; int b = 2 ; intercambiar ( &a a , & b ); printf ( "%d %d" , a , b ); // 2 1 devuelve 0 ; }
Algunos autores lo tratan &
como parte de la sintaxis de llamar swap
. Desde este punto de vista, C admite la estrategia de paso de parámetros de llamada por referencia. [47] Otros autores tienen una opinión diferente de que la implementación presentada swap
en C es solo una simulación de llamada por referencia utilizando punteros. [48] Bajo esta visión de "simulación", las variables mutables en C no son de primera clase (es decir, los valores l no son expresiones), sino más bien los tipos de puntero. Desde este punto de vista, el programa de intercambio presentado es azúcar sintáctico para un programa que utiliza punteros en todo momento, [49] por ejemplo este programa ( read
y assign
se han agregado para resaltar las similitudes con el Box
programa de llamada compartida de Java anterior):
#incluir <stdio.h> int leer ( int * p ) { retorno * p ; } asignación nula ( int * p , int v ) { * p = v ; } intercambio vacío ( int * a , int * b ) { int temp_storage ; int * temp = & temp_storage ; asignar ( temperatura , leer ( a )); asignar ( a , leer ( b )); asignar ( b , leer ( temp )); } int principal () { int un_almacenamiento ; int * a = & a_almacenamiento ; int b_almacenamiento ; int * b = & b_almacenamiento ; asignar ( a , 1 ); asignar ( b , 2 ); intercambiar ( a , b ); printf ( "%d %d" , leer ( a ), leer ( b )); // 2 1 devuelve 0 ; }
Debido a que en este programa swap
opera con punteros y no puede cambiar los punteros en sí, sino solo los valores a los que apuntan, esta visión sostiene que la principal estrategia de evaluación de C es más similar a la llamada mediante uso compartido.
C++ confunde aún más el problema al permitir swap
que se declare y utilice con una sintaxis de "referencia" muy ligera: [50]
intercambio vacío ( int & a , int & b ) { int temp = a ; a = b ; b = temperatura ; } int principal () { int a = 1 ; int b = 2 ; intercambiar ( a , b ); std :: cout << a << b << std :: endl ; // 2 1 devuelve 0 ; }
Semánticamente, esto es equivalente a los ejemplos de C. Como tal, muchos autores consideran que la llamada por dirección es una estrategia única de paso de parámetros distinta de la llamada por valor, la llamada por referencia y la llamada por uso compartido.
En programación lógica , la evaluación de una expresión puede corresponder simplemente a la unificación de los términos involucrados combinada con la aplicación de alguna forma de resolución . La unificación debe clasificarse como una estrategia vinculante estricta porque se lleva a cabo en su totalidad. Sin embargo, la unificación también se puede realizar en variables ilimitadas, por lo que es posible que las llamadas no necesariamente se comprometan con los valores finales de todas sus variables.
La llamada por nombre es una estrategia de evaluación en la que los argumentos de una función no se evalúan antes de llamar a la función; más bien, se sustituyen directamente en el cuerpo de la función (mediante la sustitución que evita la captura ) y luego se dejan para que se evalúen cada vez que aparecen en el función. Si un argumento no se utiliza en el cuerpo de la función, el argumento nunca se evalúa; si se utiliza varias veces, se reevalúa cada vez que aparece. (Consulte el dispositivo de Jensen para conocer una técnica de programación que aprovecha esto).
En ocasiones, la evaluación de llamada por nombre es preferible a la evaluación de llamada por valor. Si el argumento de una función no se utiliza en la función, la llamada por nombre ahorrará tiempo al no evaluar el argumento, mientras que la llamada por valor lo evaluará independientemente. Si el argumento es un cálculo no final, la ventaja es enorme. Sin embargo, cuando se utiliza el argumento de función, la llamada por nombre suele ser más lenta y requiere un mecanismo como un procesador .
Los lenguajes .NET pueden simular llamadas por nombre utilizando delegados o Expression<T>
parámetros. Esto último da como resultado que se le proporcione a la función un árbol de sintaxis abstracta . Eiffel proporciona agentes, que representan una operación que debe evaluarse cuando sea necesario. Seed7 proporciona llamadas por nombre con parámetros de función. Los programas Java pueden realizar una evaluación diferida similar utilizando expresiones lambda y la java.util.function.Supplier<T>
interfaz.
La llamada por necesidad es una variante memorizada de la llamada por nombre, donde, si se evalúa el argumento de la función, ese valor se almacena para su uso posterior. Si el argumento es puro (es decir, libre de efectos secundarios), esto produce los mismos resultados que llamar por nombre, ahorrando el costo de volver a calcular el argumento.
Haskell es un lenguaje muy conocido que utiliza la evaluación de llamada por necesidad. Debido a que la evaluación de expresiones puede ocurrir arbitrariamente en una etapa avanzada de un cálculo, Haskell solo admite efectos secundarios (como la mutación ) mediante el uso de mónadas . Esto elimina cualquier comportamiento inesperado de las variables cuyos valores cambian antes de su evaluación retrasada.
En la implementación de llamada por necesidad de R , se pasan todos los argumentos, lo que significa que R permite efectos secundarios arbitrarios.
La evaluación diferida es la implementación más común de la semántica de llamada por necesidad, pero existen variaciones como la evaluación optimista. Los lenguajes .NET implementan llamadas según necesidad usando el tipo Lazy<T>
.
La reducción de gráficos es una implementación eficiente de la evaluación diferida.
La llamada por expansión de macro es similar a la llamada por nombre, pero utiliza sustitución textual en lugar de sustitución que evita la captura . Por lo tanto, la macrosustitución puede dar lugar a una captura de variables, lo que lleva a errores y comportamientos no deseados. Las macros higiénicas evitan este problema al buscar y reemplazar variables sombreadas que no son parámetros.
"Llamada por futuro", también conocida como "llamada paralela por nombre" o "evaluación indulgente", [51] es una estrategia de evaluación concurrente que combina una semántica no estricta con una evaluación entusiasta. El método requiere programación y sincronización dinámicas detalladas, pero es adecuado para máquinas masivamente paralelas.
La estrategia crea un futuro (promesa) para el cuerpo de la función y cada uno de sus argumentos. Estos futuros se calculan simultáneamente con el flujo del resto del programa. Cuando un futuro A requiere el valor de otro futuro B que aún no ha sido calculado, el futuro A se bloquea hasta que el futuro B termina de calcular y tiene un valor. Si el futuro B ya ha terminado de calcular, el valor se devuelve inmediatamente. Los condicionales se bloquean hasta que se evalúa su condición y las lambdas no crean futuros hasta que se aplican por completo. [52]
Si se implementa con procesos o subprocesos, la creación de un futuro generará uno o más procesos o subprocesos nuevos (para las promesas), acceder al valor los sincronizará con el subproceso principal y finalizar el cálculo del futuro corresponde a eliminar las promesas que calculan su valor. Si se implementa con una corrutina , como en .NET async/await , la creación de un futuro llama a una corrutina (una función asíncrona), que puede ceder a la persona que llama y, a su vez, devolverse cuando se usa el valor, realizando múltiples tareas de manera cooperativa.
La estrategia no es determinista, ya que la evaluación puede ocurrir en cualquier momento entre la creación del futuro (es decir, cuando se da la expresión) y el uso del valor del futuro. La estrategia no es estricta porque el cuerpo de la función puede devolver un valor antes de que se evalúen los argumentos. Sin embargo, en la mayoría de las implementaciones, la ejecución aún puede bloquearse al evaluar un argumento innecesario. Por ejemplo, el programa
f x = 1 / x g y = 1 principal = imprimir ( g ( f 0 ))
puede tener g
un final antes f
y una salida 1, o puede generar un error debido a la evaluación 1/0
. [28]
La llamada por futuro es similar a la llamada por necesidad en que los valores se calculan solo una vez. Con un manejo cuidadoso de los errores y la no terminación, en particular la terminación de futuros a la mitad si se determina que no serán necesarios, la llamada por futuro también tiene las mismas propiedades de terminación que la evaluación de llamada por necesidad. [52] Sin embargo, la llamada por futuro puede realizar un trabajo especulativo innecesario en comparación con la llamada por necesidad, como la evaluación profunda de una estructura de datos perezosa. [28] Esto se puede evitar utilizando futuros perezosos que no inician el cálculo hasta que esté seguro de que el valor es necesario.
La evaluación optimista es una variante de llamada por necesidad en la que el argumento de la función se evalúa parcialmente en un estilo de llamada por valor durante un período de tiempo (que puede ajustarse en tiempo de ejecución ). Una vez transcurrido ese tiempo, se cancela la evaluación y se aplica la función mediante llamada por necesidad. [53] Este enfoque evita algunos de los gastos de tiempo de ejecución de las llamadas por necesidad manteniendo al mismo tiempo las características de terminación deseadas.
Probablemente fue el primer lenguaje que explotó sistemáticamente el poder de la evaluación perezosa.
{{cite book}}
: |work=
ignorado ( ayuda )