Los lenguajes de programación utilizan estrategias de evaluación para determinar dos cosas: cuándo evaluar los argumentos de una llamada de función y qué tipo de valor pasar a la función.
Para ilustrar, una aplicación de función puede evaluar el argumento antes de evaluar el cuerpo de la función y pasar la capacidad de buscar el valor actual del argumento y modificarlo mediante asignación . [1] La noción de estrategia de reducción en el cálculo lambda es similar pero distinta.
En términos prácticos, muchos lenguajes de programación modernos como C # y Java han convergido en una estrategia de evaluación llamada por valor / llamada por referencia para llamadas a funciones. [ aclaración necesaria ] Algunos lenguajes, especialmente los lenguajes de nivel inferior como C ++ , combinan varias nociones de paso de parámetros. Históricamente, la llamada por valor y la llamada por nombre se remontan a ALGOL 60 , que fue diseñado a fines de la década de 1950. PL / I y algunos sistemas Fortran utilizan la llamada por referencia . [2] Los lenguajes puramente funcionales como Haskell , así como los lenguajes no puramente funcionales como R , usan llamada por necesidad.
La estrategia de evaluación está especificada por la definición del lenguaje de programación y no es una función de ninguna implementación específica.
Evaluación estricta
En una evaluación estricta, los argumentos de una función siempre se evalúan completamente antes de que se aplique la función.
Bajo la codificación Church , la evaluación entusiasta de los operadores se correlaciona con la evaluación estricta de funciones; por esta razón, la evaluación estricta a veces se llama "ansiosa". La mayoría de los lenguajes de programación existentes utilizan una evaluación estricta de las funciones.
Orden de aplicación
La evaluación de orden aplicativo es una estrategia de evaluación en la que una expresión se evalúa evaluando repetidamente su expresión reducible más interna a la izquierda . Esto significa que los argumentos de una función se evalúan antes de que se aplique la función. [3]
Llamar por valor
La llamada por valor (también conocida como paso por valor) es la estrategia de evaluación más común, utilizada en lenguajes tan diferentes como C y Scheme . En la llamada por valor, la expresión del argumento se evalúa y el valor resultante 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 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 del llamador cuando la función regresa.
La llamada por valor no es una estrategia de evaluación única, sino la familia de estrategias de evaluación en las que se evalúa el argumento de una función antes de pasar a la función. Mientras que muchos lenguajes de programación (como Common Lisp, Eiffel y Java) que usan llamada por valor evalúan los argumentos de las funciones de izquierda a derecha, algunos evalúan funciones y sus argumentos de derecha a izquierda, y otros (como Scheme, OCaml y C ) no especifican el orden.
Limitaciones implícitas
En algunos casos, el término "llamada por valor" es problemático, ya que el valor que se pasa no es el valor de la variable como se entiende por el significado ordinario de valor, sino una referencia específica de implementación al valor. El efecto es que lo que sintácticamente parece llamada por valor puede terminar comportándose más bien como llamada por referencia o llamada por compartir , a menudo dependiendo de aspectos muy sutiles de la semántica del lenguaje.
La razón para pasar una referencia es a menudo que el lenguaje técnicamente no proporciona una representación de valor de datos complicados, sino que los representa como una estructura de datos mientras conserva una apariencia de apariencia de valor en el código fuente. A menudo es difícil predecir exactamente dónde se traza el límite entre los valores adecuados y las estructuras de datos que se disfrazan como tales. En C , una matriz (cuyas cadenas son casos especiales) es una estructura de datos, pero el nombre de una matriz se trata como (tiene como valor) la referencia al primer elemento de la matriz, mientras que el nombre de una variable de estructura se refiere a un valor. incluso si tiene campos que son vectores. En Maple , un vector es un caso especial de una tabla y, por lo tanto, una estructura de datos, pero una lista (que se representa y se puede indexar exactamente de la misma manera) es un valor. En Tcl , los valores son de "doble puerto", de modo que la representación del valor se utiliza en el nivel de la secuencia de comandos y el propio lenguaje gestiona la estructura de datos correspondiente, si es necesario. Las modificaciones realizadas a través de la estructura de datos se reflejan en la representación del valor y viceversa.
La descripción "llamada por valor donde el valor es una referencia" es común (pero no debe entenderse como llamada por referencia); otro término es llamar por compartir . Por lo tanto, el comportamiento de la llamada por valor Java o Visual Basic y la llamada por valor C o Pascal son significativamente diferentes: en C o Pascal, llamar a una función con una estructura grande como argumento hará que se copie toda la estructura (excepto si en realidad es una referencia a una estructura), lo que podría causar una degradación grave del rendimiento, y las mutaciones en la estructura son invisibles para el llamador. Sin embargo, en Java o Visual Basic solo se copia la referencia a la estructura, lo que es rápido y las mutaciones en la estructura son visibles para el llamador.
Llamar por referencia
Llamar por referencia (o pasar por referencia) es una estrategia de evaluación en la que una función recibe una referencia implícita a una variable utilizada como argumento, en lugar de una copia de su valor.
Por lo general, esto significa que la función puede modificar (es decir, asignar a ) la variable utilizada como argumento, algo que verá 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. 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. Una simple prueba de fuego para determinar si un lenguaje admite la semántica de llamada por referencia es si es posible escribir una swap(a, b)
función tradicional en el lenguaje. [4]
Muchos idiomas admiten la llamada por referencia de alguna forma, pero pocos la utilizan de forma predeterminada. FORTRAN II es un ejemplo temprano de lenguaje de llamada por referencia. Algunos lenguajes, como C ++ , PHP , Visual Basic .NET , C # y REALbasic , se llaman de forma predeterminada por valor, pero ofrecen una sintaxis especial para los parámetros de llamada por referencia. C ++ también ofrece llamadas por referencia a const .
La llamada por referencia se puede simular en lenguajes que usan llamada por valor y no admiten exactamente la llamada por referencia, haciendo uso de referencias (objetos que hacen referencia a otros objetos), como punteros (objetos que representan las direcciones de memoria de otros objetos) . Idiomas como C , ML y Rust utilizan esta técnica. No es una estrategia de evaluación separada —el lenguaje llama por valor— pero a veces se la denomina "llamar por dirección" o "pasar por dirección". En ML, las referencias son seguras para el tipo y la memoria , similar a Rust.
Se logra un efecto similar al llamar compartiendo (pasando un objeto, que luego puede ser mutado), usado en lenguajes como Java, Python y Ruby .
En los lenguajes puramente funcionales, normalmente no hay diferencia semántica entre las dos estrategias (dado que sus estructuras de datos son inmutables, por lo que no hay posibilidad de que una función modifique ninguno de sus argumentos), por lo que normalmente se describen como llamada por valor aunque las implementaciones utilice con frecuencia la llamada por referencia internamente para obtener beneficios de eficiencia.
A continuación se muestra un ejemplo que demuestra la llamada por referencia en el lenguaje de programación E :
def modificar (var p, & q) { p: = 27 # pasado por valor: solo se modifica el parámetro local q: = 27 # pasado por referencia: se modifica la variable utilizada en la llamada}? var a: = 1# valor: 1? var b: = 2# valor: 2? modificar (a, y b)? a# valor: 1? B# valor: 27
A continuación se muestra un ejemplo de llamada por dirección que simula una llamada por referencia en C :
modificar vacío ( int p , int * q , int * r ) { p = 27 ; // pasado por valor: solo se modifica el parámetro local * q = 27 ; // pasado por valor o referencia, verifique el sitio de la llamada para determinar cuál * r = 27 ; // pasado por valor o referencia, verifique el sitio de la llamada para determinar cuál }int main () { int a = 1 ; int b = 1 ; int x = 1 ; int * c = & x ; modificar ( a , & b , c ); // a se pasa por valor, b se pasa por referencia creando un puntero (llamada por valor), // c es un puntero pasado por valor // b y x se cambian devuelve 0 ; }
Llamar compartiendo
La llamada por compartir (también conocida como "llamada por objeto" o "llamada por intercambio de objetos") es una estrategia de evaluación que Barbara Liskov señaló por primera vez en 1974 para el lenguaje CLU . [5] Es utilizado por lenguajes como Python , [6] Java (para referencias de objetos), Ruby , JavaScript , Scheme, OCaml, AppleScript y muchos otros. Sin embargo, el término "llamar compartiendo" no es de uso común; la terminología es inconsistente en diferentes fuentes. Por ejemplo, en la comunidad Java, dicen que Java se llama por valor. [7] Llamar por compartir implica que los valores en el lenguaje se basan en objetos en lugar de tipos primitivos , es decir, que todos los valores están " encuadrados ". Debido a que están encuadrados, se puede decir que pasan por copia de la referencia (donde las primitivas están encuadradas antes de pasar y desempaquetadas en la función llamada).
La semántica de la llamada por compartir difiere de la llamada por referencia: "En particular, no se llama por valor porque las mutaciones de los argumentos realizados por la rutina llamada serán visibles para el llamador. Y no se llama por referencia porque no se da acceso a las variables de la persona que llama, pero simplemente a ciertos objetos ". [8] Entonces, por ejemplo, si se pasó una variable, no es posible simular una asignación en esa variable en el alcance del destinatario. [9] Sin embargo, dado que la función tiene acceso al mismo objeto que la persona que llama (no se realiza ninguna copia), las mutaciones en esos objetos, si los objetos son mutables , dentro de la función son visibles para la persona que llama, lo que puede parecer diferente de llamada por semántica de valor. 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, sino que se comparte.
Por ejemplo, en Python, las listas son mutables, entonces:
def f ( una_lista ): una_lista . añadir ( 1 )m = [] f ( m ) imprimir ( m )
salidas [1]
porque el append
método modifica el objeto en el que se llama.
Las asignaciones dentro de una función no son perceptibles para la persona que llama, porque, en estos lenguajes, pasar la variable solo significa pasar (acceder a) el objeto real al que hace referencia la variable, no acceder a la variable original (de la persona que llama). Dado que la variable de rebote solo existe dentro del alcance de la función, la contraparte del llamador conserva su enlace original.
Compare la mutación de Python anterior con el código a continuación, que une el argumento formal a un nuevo objeto:
def f ( a_list ): a_list = [ 1 ]m = [] f ( m ) imprimir ( m )
salidas []
, porque la declaración a_list = [1]
reasigna una nueva lista a la variable en lugar de a la ubicación a la que hace referencia.
Para los 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 idioma. 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 a (el argumento no se sobrescribe y la identidad del objeto no se cambia), pero el objeto (argumento) está mutado. [10]
Aunque este término tiene un uso generalizado en la comunidad de Python, la semántica idéntica en otros lenguajes como Java y Visual Basic a menudo se describe como llamada por valor, donde se implica que el valor es una referencia al objeto. [ cita requerida ]
Llamar por copia-restauración
Llamada por copia-restauración, también conocida como "copia de copia de salida", "llamada por resultado de valor", "llamada por valor de retorno" (como se denomina en la comunidad de Fortran ), es un caso especial de llamada por referencia donde el La referencia proporcionada es única para la persona que llama. Esta variante ha llamado la atención en contextos de multiprocesamiento y llamadas a procedimientos remotos : [11] si un parámetro de una llamada a una función es una referencia a la que puede acceder otro hilo de ejecución, su contenido puede copiarse a una nueva referencia que no lo es; cuando vuelve la llamada a la función, el contenido actualizado de esta nueva referencia se copia de nuevo a la referencia original ("restaurada").
La semántica de llamada por copia-restauración también difiere de la de llamada por referencia, donde dos o más argumentos de función se alían entre sí (es decir, apuntan a la misma variable en el entorno del llamador). Bajo llamada por referencia, escribir a uno afectará al otro; la llamada mediante copia-restauración evita esto dando a la función copias distintas, pero deja el resultado en el entorno de la persona que llama indefinido dependiendo de cuál de los argumentos con alias se vuelva a copiar primero; las copias se harán en orden de izquierda a derecha tanto en la entrada y al regreso?
Cuando la referencia se pasa al destinatario de la llamada sin inicializar, esta estrategia de evaluación puede denominarse "llamada por resultado".
Evaluación parcial
En la evaluación parcial, la evaluación puede continuar en el cuerpo de una función que no se ha aplicado. Se evalúan todas las subexpresiones que no contienen variables independientes y se pueden reducir las aplicaciones de función cuyos valores de argumento se conocen. Si hay efectos secundarios , la evaluación parcial completa puede producir resultados no deseados, por lo que los sistemas que apoyan la evaluación parcial tienden a hacerlo solo para expresiones "puras" (es decir, aquellas sin efectos secundarios) dentro de las funciones.
Evaluación no estricta
En la evaluación no estricta, los argumentos de una función no se evalúan a menos que se utilicen realmente en la evaluación del cuerpo de la función.
Bajo la codificación Church , la evaluación perezosa de los operadores se asigna a la evaluación no estricta de funciones; por esta razón, la evaluación no estricta a menudo se denomina "perezosa". Las expresiones booleanas en muchos lenguajes usan una forma de evaluación no estricta llamada evaluación de cortocircuito , donde la evaluación regresa tan pronto como se puede determinar que resultará un booleano inequívoco, por ejemplo, en una expresión disyuntiva (OR) donde true
se encuentra, o en una expresión conjuntiva (Y) donde false
se encuentra, y así sucesivamente. Las expresiones condicionales también suelen utilizar la evaluación diferida, donde la evaluación devuelve tan pronto como resulte una rama inequívoca.
Orden normal
La evaluación de orden normal es una estrategia de evaluación en la que una expresión se evalúa evaluando repetidamente su expresión reducible más externa a la izquierda . Esto significa que los argumentos de una función no se evalúan antes de que se aplique la función. [12]
Llamar por nombre
Llamar por nombre es una estrategia de evaluación en la que los argumentos de una función no se evalúan antes de que se llame a la función; más bien, se sustituyen directamente en el cuerpo de la función (utilizando la sustitución para evitar la captura ) y luego se dejan para ser evaluados cada vez que aparecen en el función. Si no se usa un argumento en el cuerpo de la función, el argumento nunca se evalúa; si se usa varias veces, se reevalúa cada vez que aparece. (Ver Dispositivo de Jensen) .
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 usa 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 terminante, la ventaja es enorme. Sin embargo, cuando se usa el argumento de la función, la llamada por nombre suele ser más lenta y requiere un mecanismo como un procesador .
Uno de los primeros usos fue ALGOL 60 . Los lenguajes .NET actuales pueden simular llamadas por nombre utilizando delegados o Expression
parámetros. El último da como resultado un árbol de sintaxis abstracto que se le da a la función. Eiffel proporciona agentes, que representan una operación a evaluar cuando sea necesario. Seed7 proporciona llamadas por nombre con parámetros de función. Los programas Java pueden realizar evaluaciones perezosas similares utilizando expresiones lambda y la java.util.function.Supplier
interfaz.
Llame por necesidad
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, lo que ahorra el costo de volver a calcular el argumento.
Haskell es un lenguaje bien conocido que utiliza la evaluación de llamada por necesidad. Debido a que la evaluación de expresiones puede suceder 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 R de la llamada por necesidad, 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 por necesidad usando el tipo Lazy
.
Llamar por macro expansión
La llamada por macro expansión es similar a la llamada por nombre, pero utiliza la sustitución textual en lugar de la captura, evitando así la sustitución. Pero la sustitución de macros puede provocar errores, lo que resulta en la captura de variables , lo que lleva a un comportamiento no deseado. Las macros higiénicas evitan este problema comprobando y reemplazando variables sombreadas que no son parámetros.
Estrategias no deterministas
Reducción β completa
En " reducción β completa ", cualquier aplicación de función puede reducirse (sustituyendo el argumento de la función en la función utilizando la sustitución para evitar la captura) en cualquier momento. Esto se puede hacer incluso dentro del cuerpo de una función no aplicada.
Llamar por futuro
"Llamada por futuro", también conocida como "llamada paralela por nombre", es una estrategia de evaluación concurrente en la que el valor de una expresión futura se calcula al mismo tiempo que el flujo del resto del programa con promesas, también conocido como futuros. Cuando se necesita el valor de la promesa, el programa principal se bloquea hasta que la promesa tiene un valor (la promesa o una de las promesas termina de computarse, si aún no se ha completado).
Esta 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. Es similar a la llamada por necesidad en que el valor solo se calcula una vez y el cálculo puede aplazarse hasta que se necesite el valor, pero puede iniciarse antes. Además, si el valor de un futuro no es necesario, como si se tratara de una variable local en una función que retorna, el cálculo puede terminarse a la mitad.
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 terminar el cálculo del futuro corresponde a matar las promesas calculando 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 al llamador y, a su vez, ceder cuando se usa el valor, cooperativamente para realizar múltiples tareas.
Evaluación optimista
La evaluación optimista es otra variante de llamada por necesidad en la que el argumento de la función se evalúa parcialmente durante un período de tiempo (que puede ajustarse en tiempo de ejecución ). Una vez transcurrido ese tiempo, la evaluación se cancela y la función se aplica mediante la llamada por necesidad. [13] Este enfoque evita algunos de los gastos de tiempo de ejecución de la estrategia de llamada por necesidad al tiempo que conserva las características de terminación deseadas.
Ver también
- Forma beta normal
- Comparación de lenguajes de programación
- eval
- Cálculo lambda
- Valor de llamada por empuje
- Parámetro (informática)
Referencias
- ^ Daniel P. Friedman; Varita Mitchell (2008). Fundamentos de los lenguajes de programación (tercera ed.). Cambridge, MA: The MIT Press . ISBN 978-0262062794.
- ^ Algunos sistemas Fortran usan llamada por copia-restauración.
- ^ "Reducción de pedido aplicable" . Encyclopedia2.thefreedictionary.com . Consultado el 19 de noviembre de 2019 .
- ^ "¡Java es Pass-by-Value, Maldita sea!" . Consultado el 24 de diciembre de 2016 .
- ^ Liskov, Barbara; Atkinson, Russ; Bloom, Toby; Moss, Eliot; Schaffert, Craig; Scheifler, Craig; Snyder, Alan (octubre de 1979). "Manual de referencia de CLU" (PDF) . Laboratorio de Ciencias de la Computación . Instituto de Tecnología de Massachusetts. Archivado desde el original (PDF) el 22 de septiembre de 2006 . Consultado el 19 de mayo de 2011 .
- ^ Lundh, Fredrik. "Llamar por objeto" . effbot.org . Consultado el 19 de mayo de 2011 .
- ^ "¡Java es Pass-by-Value, Maldita sea!" . Consultado el 24 de diciembre de 2016 .
- ^ Manual de referencia de CLU (1974) , p. 14-15.
- ^ Nota: en lenguaje CLU, "variable" corresponde a "identificador" y "puntero" en el uso estándar moderno, no al significado general / habitual de variable .
- ^ "CA1021: Evite los parámetros de salida" . Microsoft.
- ^ "RPC: Especificación de protocolo de llamada a procedimiento remoto versión 2" . tools.ietf.org . IETF . Consultado el 7 de abril de 2018 .
- ^ "Reducción de pedido normal" . Encyclopedia2.thefreedictionary.com . Consultado el 19 de noviembre de 2019 .
- ^ Ennals, Robert; Jones, Simon Peyton (agosto de 2003). "Evaluación optimista: una estrategia de evaluación rápida para programas no estrictos" .
Otras lecturas
- Abelson, Harold ; Sussman, Gerald Jay (1996). Estructura e interpretación de programas informáticos (Segunda ed.). Cambridge, Massachusetts: The MIT Press. ISBN 978-0-262-01153-2.
- Baker-Finch, Clem; Rey David; Hall, Jon; Trinder, Phil (10 de marzo de 1999). "Una semántica operacional para llamada paralela por necesidad" (ps) . Informe de investigación . Facultad de Matemáticas y Computación, The Open University. 99 (1).
- Ennals, Robert; Peyton Jones, Simon (2003). Evaluación optimista: una estrategia de evaluación rápida para programas no estrictos (PDF) . Congreso Internacional de Programación Funcional. Prensa ACM.
- Ludäscher, Bertram (24 de enero de 2001). "Apuntes de la conferencia CSE 130" . CSE 130: Lenguajes de programación: principios y paradigmas .
- Pierce, Benjamin C. (2002). Tipos y lenguajes de programación . Prensa del MIT . ISBN 0-262-16209-1.
- Sestoft, Peter (2002). Mogensen, T; Schmidt, D; Sudborough, IH (eds.). Demostración de la reducción del cálculo de Lambda (PDF) . La esencia de la computación: complejidad, análisis, transformación. Ensayos dedicados a Neil D. Jones . Apuntes de conferencias en informática. 2566 . Springer-Verlag. págs. 420–435. ISBN 3-540-00326-6.
- "Llamada por valor y llamada por referencia en programación C" . Llamada por valor y llamada por referencia en C Programación explicó . Archivado desde el original el 21 de enero de 2013.