En un lenguaje de programación , una estrategia de evaluación es un conjunto de reglas para evaluar expresiones. [1] El término se utiliza 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 se deben evaluar los parámetros de una llamada de función, y si es así en qué orden (el orden de evaluación ). [4] La noción de estrategia de reducción es distinta, [5] aunque algunos autores confunden los dos términos y la definición de cada término no es ampliamente aceptada. [6]
Para ilustrar, la ejecución de una llamada de función f(a,b)
puede evaluar primero 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 la asignación como si fueran variables locales y devolver valores mediante 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 lenguajes definen una convención de llamada . [ Aclaración necesaria ]
Esta es una tabla de estrategias de evaluación e idiomas representativos por año de introducción. Los idiomas representativos se enumeran en orden cronológico, comenzando con el idioma o idiomas que introdujeron la estrategia y seguidos por los idiomas destacados que la utilizan. [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 :
sea f x = print_int x ; x ;; print_int ( f 1 + f 2 )
resultados 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 lenguaje como C++ tradicionalmente dejaban el orden sin especificar, aunque lenguajes como Java y C# definen el orden de evaluación como de izquierda a derecha [8] : 240–241 y el estándar C++17 ha agregado restricciones al 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 que se aplique 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 la estrategia de enlace de llamada por valor que 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 sin definir. [25] Scheme requiere que el orden de ejecución sea la ejecución secuencial de una permutación no especificada de los argumentos. [26] OCaml 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 se necesitan 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 otro 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 hay una (es una estrategia de reducción "normalizadora" ). [31] La evaluación perezosa 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 estrictez con la evaluación perezosa. [28] : 43–44
Las expresiones booleanas en muchos lenguajes utilizan una forma de evaluación no estricta llamada evaluación de cortocircuito , donde la evaluación evalúa la expresión izquierda pero puede omitir la expresión derecha si el resultado puede determinarse, por ejemplo, en una expresión disyuntiva (OR) donde true
se encuentra, o en una expresión conjuntiva (AND) 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]
En 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] lo que permite la especificación de construcciones de flujo de control definidas por el usuario, una función que no está disponible en la evaluación de orden aplicativo. La evaluación de orden normal utiliza estructuras complejas, como thunks , para expresiones no evaluadas, en comparación con la pila de llamadas utilizada en la evaluación de orden aplicativo. [33] La evaluación de orden normal históricamente ha carecido de herramientas de depuración utilizables debido a su complejidad. [34]
En la llamada por valor (o paso por valor), el valor evaluado de la expresión del argumento se vincula a la variable correspondiente en la función (con frecuencia copiando el valor en una nueva región de memoria). Si la función o el 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 se modifica en el ámbito del invocador cuando la función retorna. 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 el invocador: [35]
programa Principal ; utiliza crt ; procedimiento PrintArray ( a : Matriz de enteros ) ; var i : Entero ; inicio para i := Low ( a ) a High ( a ) hacer Write ( a [ i ]) ; WriteLn () ; fin ; Procedimiento Modificar ( Fila : Matriz de enteros ) ; comenzar PrintArray ( Fila ) ; // 123 Fila [ 1 ] := 4 ; PrintArray ( Fila ) ; // 143 terminar ; Var A : Matriz de enteros ; inicio A := [ 1 , 2 , 3 ] ; PrintArray ( A ) ; // 123 Modificar ( A ) ; PrintArray ( A ) ; // 123 fin .
Estrictamente hablando, bajo la llamada por valor, ninguna operación realizada por la rutina llamada puede ser visible para el llamador, 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, la circunloquio "llamada por valor donde el valor es una referencia" se ha vuelto común en algunos lenguajes, por ejemplo, la comunidad Java. [36] Comparado con el paso por valor tradicional, el valor que se pasa no es un valor como se entiende por el significado ordinario de valor, como un entero que puede escribirse como un literal, sino un identificador de referencia interno de la implementación . Las mutaciones a este identificador de referencia son visibles en el llamador. Debido a la mutación visible, esta forma de "llamada por valor" se conoce más apropiadamente como llamada por compartir. [16]
En lenguajes puramente funcionales , los valores y las estructuras de datos son inmutables, por lo que no existe la posibilidad de que una función modifique ninguno de sus argumentos. Por lo tanto, 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 suelen utilizar la llamada por referencia internamente para obtener beneficios de eficiencia. No obstante, estos lenguajes suelen describirse como lenguajes de llamada por valor.
La llamada por referencia (o paso por referencia) es una estrategia de evaluación en la que un parámetro está ligado a una referencia implícita a la variable utilizada como argumento, en lugar de una copia de su valor. Esto normalmente significa que la función puede modificar (es decir, asignar a ) la variable utilizada como argumento, algo que será visto por su llamador. 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. El paso 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 de función y puede introducir errores sutiles.
Debido a la variación en la sintaxis, la diferencia entre una llamada por referencia (donde el tipo de referencia es implícito) y una llamada por uso compartido (donde el tipo de referencia es explícito) a menudo no está clara a primera vista. Una prueba de fuego sencilla es si es posible escribir una swap(a, b)
función tradicional en el lenguaje. [36] Por ejemplo, en Fortran:
programa Principal implícito ninguno entero :: a = 1 entero :: b = 2 llamada Swap ( a , b ) imprimir * , a , b ! 2 1 contiene subrutina Swap ( a , b ) entero , intento ( inout ) :: a , b entero :: temp temp = a a = b b = temp fin subrutina Swap fin 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. En cambio, lo más cercano que se puede llegar a esto en Java es:
clase Main { static class Caja { int valor ; public Caja ( int valor ) { this . valor = valor ; } } static void swap ( Caja a , Caja b ) { int temp = a . valor ; a . valor = b . valor ; b . valor = temp ; } public static void main ( String [] args ) { Caja a = new Caja ( 1 ); Caja b = new Caja ( 2 ); swap ( a , b ); System . out . 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 de llamada por intercambio, pero no de llamada por referencia. [36]
La llamada por copia-restauración, también conocida como "copia de entrada, copia de salida", "llamada por resultado de valor", "llamada por retorno de valor" (como se denomina en la comunidad Fortran ), es una variación de la llamada por referencia. Con la llamada por copia-restauración, el contenido del argumento se copia a una nueva variable local a la invocación de la llamada. 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 la llamada de la función retorna, el contenido actualizado de esta variable se copia nuevamente para sobrescribir el argumento original ("restaurado"). [37]
La semántica de la llamada por 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 alias entre sí (es decir, apuntan a la misma variable en el entorno del llamador). Bajo la llamada por referencia, escribir en un argumento afectará al otro durante la ejecución de la función. Bajo la llamada por 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 del llamador. [38] Por ejemplo, Ada especifica que la asignación de copia de salida para cada parámetro in out
o out
ocurre en un orden arbitrario. [39] Del 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 ;procedimiento Test_Copy_Restore es procedimiento Modificar ( A , B : in out Integer ) es begin A := A + 1 ; B := B + 2 ; end Modificar ; X : Integer := 0 ; begin Modificar ( X , X ); Put_Line ( "X = " & Integer ' Image ( X )); end Test_Copy_Restore ; -- $ gnatmake -gnatd.E test_copy_restore.adb; ./test_copy_restore -- test_copy_restore.adb:12:10: advertencia: el valor real escribible para "A" se superpone con el valor real para "B" [-gnatw.i] -- X = 2
Si el programa devolviera 1, estaría copiando de derecha a izquierda y, bajo la semántica de llamada por referencia, el programa devolvería 3.
Cuando la referencia se pasa al llamador 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 el multiprocesamiento y en las llamadas a procedimientos remotos , [41] ya que, a diferencia de la llamada por referencia, no requiere una comunicación frecuente entre subprocesos de ejecución para el acceso a las variables.
La llamada por compartición (también conocida como "pasar por compartición", "llamada por objeto" o "llamada por compartición de objetos") 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 una referencia, solo una clase específica de valores, denominada "referencias", " tipos encajonados " 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 realiza la llamada. Al igual que la llamada por referencia, la mutación del objetivo del puntero es visible para la función que realiza la llamada. Las mutaciones de un objeto mutable dentro de la función son visibles para el que realiza la llamada porque el objeto no se copia ni se clona, se comparte , de ahí el nombre "llamada por compartición". [16]
La técnica fue descrita por primera vez por Barbara Liskov en 1974 para el lenguaje CLU . [16] La utilizan 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 "llamada por compartición" tal como se utiliza en este artículo no es de uso común; la terminología es inconsistente en diferentes fuentes. Por ejemplo, en la comunidad Java, dicen que Java es una llamada por valor. [36]
En el caso de objetos inmutables , no existe una diferencia real entre la llamada por compartición y la llamada por valor, excepto si la identidad del objeto es visible en el lenguaje. El uso de la llamada por compartición 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 modifica), pero el objeto (argumento) se muta. [45]
Por ejemplo, en Python, las listas son mutables y se pasan con llamadas compartidas, así:
def f ( una_lista ) : una_lista .append ( 1 )m = [] f ( m ) imprimir ( m )
salidas [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 visibles para el que llama. Por ejemplo, este código vincula el argumento formal a un nuevo objeto, pero no es visible para el 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 en el que 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> void swap ( int * a , int * b ) { int temp = * a ; * a = * b ; * b = temp ; } int main () { int a = 1 ; int b = 2 ; intercambiar ( &a a , & b ); printf ( "%d %d" , a , b ); // 2 1 devuelve 0 ; }
Algunos autores tratan &
como parte de la sintaxis de la llamada swap
. Bajo esta perspectiva, C admite la estrategia de paso de parámetros de llamada por referencia. [47] Otros autores adoptan una visión diferente de que la implementación presentada de 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 que lo son los tipos de puntero. En esta visión, 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 por intercambio de Java anterior):
#incluir <stdio.h> int leer ( int * p ) { devolver * p ; } void asignar ( int * p , int v ) { * p = v ; } void swap ( int * a , int * b ) { int temp_storage ; int * temp = & temp_storage ; asignar ( temp , leer ( a )); asignar ( a , leer ( b )); asignar ( b , leer ( temp )); } int main () { int a_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 devolver 0 ; }
Debido a que en este programa swap
se opera sobre punteros y no se pueden cambiar los punteros en sí, sino solamente los valores a los que apuntan los punteros, este punto de vista sostiene que la estrategia de evaluación principal de C es más similar a la llamada por intercambio.
C++ complica aún más la cuestión al permitir swap
que se declare y utilice con una sintaxis de "referencia" muy ligera: [50]
void swap ( int & a , int & b ) { int temp = a ; a = b ; b = temp ; } int main () { int a = 1 ; int b = 2 ; intercambiar ( a , b ); std :: cout << a << b << std :: endl ; // 2 1 devolver 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 de paso de parámetros única, 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 de vinculación estricta porque se realiza en su totalidad. Sin embargo, la unificación también se puede realizar en variables no acotadas, por lo que las llamadas pueden no necesariamente comprometerse con los valores finales para 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, sino que 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 la función. Si un argumento no se utiliza en el cuerpo de la función, nunca se evalúa; si se utiliza varias veces, se vuelve a evaluar cada vez que aparece. (Consulte el dispositivo de Jensen para obtener una técnica de programación que aprovecha esto).
En ocasiones, la evaluación por nombre es preferible a la evaluación 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á de todas formas. Si el argumento es un cálculo no terminal, la ventaja es enorme. Sin embargo, cuando se utiliza el argumento de la función, la llamada por nombre suele ser más lenta y requiere un mecanismo como un thunk .
Los lenguajes .NET pueden simular llamadas por nombre utilizando delegados o Expression<T>
parámetros. Esto último da como resultado un árbol de sintaxis abstracta que se le proporciona a la función. Eiffel proporciona agentes, que representan una operación que se evaluará cuando sea necesario. Seed7 proporciona llamadas por nombre con parámetros de función. Los programas Java pueden lograr 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 la llamada por nombre, ahorrando el costo de volver a calcular el argumento.
Haskell es un lenguaje conocido que utiliza la evaluación por necesidad. Debido a que la evaluación de expresiones puede ocurrir arbitrariamente en un momento dado 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 perezosa 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 la llamada por necesidad utilizando el tipo Lazy<T>
.
La reducción de gráficos es una implementación eficiente de la evaluación perezosa.
La expansión de llamadas por macro es similar a la llamada por nombre, pero utiliza la sustitución textual en lugar de la sustitución que evita la captura . Por lo tanto, la sustitución de macro puede dar como resultado la captura de variables, lo que genera errores y un comportamiento no deseado. Las macros higiénicas evitan este problema al verificar y reemplazar las variables sombreadas que no son parámetros.
La "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 diligente. El método requiere una programación y sincronización dinámicas de grano fino, 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 se ha calculado, el futuro A se bloquea hasta que el futuro B termina de calcularse y tiene un valor. Si el futuro B ya ha terminado de calcularse, 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), el acceso al valor los sincronizará con el subproceso principal y la finalización del cálculo del futuro corresponde a la eliminación de 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 asincrónica), que puede ceder el paso al que llama y, a su vez, ceder el paso al que llama 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 proporciona 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 puede quedarse atascada al evaluar un argumento innecesario. Por ejemplo, el programa
f x = 1 / x g y = 1 principal = imprimir ( g ( f 0 ))
puede g
terminar antes f
y generar 1, o puede generar un error debido a la evaluación de 1/0
. [28]
La llamada por futuro es similar a la llamada por necesidad en el sentido de que los valores se calculan solo una vez. Con un manejo cuidadoso de los errores y la no terminación, en particular terminando los futuros a mitad de camino 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 comienzan el cálculo hasta que se tiene la certeza 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 se puede ajustar 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 la llamada por necesidad al tiempo que conserva las características de terminación deseadas.
el primer lenguaje en explotar sistemáticamente el poder de la evaluación perezosa.
{{cite book}}
: |work=
ignorado ( ayuda )