En los lenguajes de programación , un cierre , también cierre léxico o cierre de función , es una técnica para implementar el enlace de nombres de ámbito léxico en un lenguaje con funciones de primera clase . Desde el punto de vista operativo , un cierre es un registro que almacena una función [a] junto con un entorno. [1] El entorno es un mapeo que asocia cada variable libre de la función (variables que se usan localmente, pero definidas en un ámbito adjunto) con el valor o referencia.al que estaba vinculado el nombre cuando se creó el cierre. [b] A diferencia de una función simple, un cierre permite que la función acceda a esas variables capturadas a través de las copias del cierre de sus valores o referencias, incluso cuando la función se invoca fuera de su alcance.
Historia y etimología
El concepto de cierres se desarrolló en la década de 1960 para la evaluación mecánica de expresiones en el cálculo λ y se implementó por primera vez en 1970 como una característica del lenguaje en el lenguaje de programación PAL para admitir funciones de primera clase con ámbito léxico . [2]
Peter J. Landin definió el término cierre en 1964 como que tiene una parte de entorno y una parte de control como lo usa su máquina SECD para evaluar expresiones. [3] Joel Moses le da crédito a Landin por introducir el término cierre para referirse a una expresión lambda cuyas vinculaciones abiertas (variables libres) han sido cerradas (o vinculadas) por el entorno léxico, resultando en una expresión cerrada o clausura. [4] [5] Este uso fue posteriormente adoptado por Sussman y Steele cuando definieron Scheme en 1975, [6] una variante léxica de Lisp y se generalizó.
Sussman y Abelson también usan el término cierre en la década de 1980 con un segundo significado no relacionado: la propiedad de un operador que agrega datos a una estructura de datos para también poder agregar estructuras de datos anidadas. Este uso del término proviene del uso de las matemáticas más que del uso anterior en la informática. Los autores consideran que esta superposición de terminología es "desafortunada". [7]
Funciones anónimas
El término cierre se usa a menudo como sinónimo de función anónima , aunque estrictamente, una función anónima es una función literal sin un nombre, mientras que un cierre es una instancia de una función, un valor , cuyas variables no locales se han vinculado a valores o ubicaciones de almacenamiento (según el idioma; consulte la sección de entorno léxico a continuación).
Por ejemplo, en el siguiente código de Python :
def f ( x ): def g ( y ): return x + y return g # Devuelve un cierre.def h ( x ): return lambda y : x + y # Devuelve un cierre.# Asignación de cierres específicos a variables. a = f ( 1 ) b = h ( 1 )# Utilizando los cierres almacenados en variables. afirmar a ( 5 ) == 6 afirmar b ( 5 ) == 6# Usar cierres sin vincularlos a variables primero. afirmar f ( 1 ) ( 5 ) == 6 # f (1) es el cierre. afirmar h ( 1 ) ( 5 ) == 6 # h (1) es el cierre.
los valores de a
y b
son cierres, en ambos casos producidos al devolver una función anidada con una variable libre de la función envolvente, de modo que la variable libre se une al valor del parámetro x
de la función envolvente. Los cierres en a
y b
son funcionalmente idénticos. La única diferencia en la implementación es que en el primer caso usamos una función anidada con un nombre, g
mientras que en el segundo caso usamos una función anónima (usando la palabra clave Python lambda
para crear una función anónima). El nombre original, si lo hay, utilizado para definirlos es irrelevante.
Un cierre es un valor como cualquier otro valor. No es necesario asignarlo a una variable y, en cambio, se puede usar directamente, como se muestra en las dos últimas líneas del ejemplo. Este uso puede considerarse un "cierre anónimo".
Las definiciones de funciones anidadas no son cierres en sí mismas: tienen una variable libre que aún no está vinculada. Solo una vez que la función de inclusión se evalúa con un valor para el parámetro, la variable libre de la función anidada se enlaza, creando un cierre, que luego se devuelve desde la función de inclusión.
Por último, un cierre solo es distinto de una función con variables libres cuando está fuera del alcance de las variables no locales; de lo contrario, el entorno de definición y el entorno de ejecución coinciden y no hay nada que los distinga (el enlace estático y dinámico no se puede distinguir porque los nombres se resuelven con los mismos valores). Por ejemplo, en el programa a continuación, las funciones con una variable libre x
(vinculada a la variable no local x
con alcance global) se ejecutan en el mismo entorno donde x
se define, por lo que es indiferente si se trata de cierres:
x = 1 nums = [ 1 , 2 , 3 ]def f ( y ): devuelve x + ymapa ( f , nums ) mapa ( lambda y : x + y , nums )
Esto se logra con mayor frecuencia mediante un retorno de función, ya que la función debe definirse dentro del alcance de las variables no locales, en cuyo caso normalmente su propio alcance será más pequeño.
Esto también se puede lograr mediante el sombreado de variables (que reduce el alcance de la variable no local), aunque esto es menos común en la práctica, ya que es menos útil y se desaconseja el sombreado. En este ejemplo, f
se puede ver que es un cierre porque x
en el cuerpo de f
está vinculado al x
en el espacio de nombres global, no al x
local a g
:
x = 0def f ( y ): devuelve x + ydef g ( z ): x = 1 # local x sombras global x return f ( z )g ( 1 ) # evalúa a 1, no a 2
Aplicaciones
El uso de cierres está asociado con lenguajes donde las funciones son objetos de primera clase , en los que las funciones pueden devolverse como resultados de funciones de orden superior o pasarse como argumentos a otras llamadas a funciones; si las funciones con variables libres son de primera clase, devolver una crea un cierre. Esto incluye lenguajes de programación funcionales como Lisp y ML , así como muchos lenguajes modernos de múltiples paradigmas, como Python y Rust . Los cierres también se usan con frecuencia con devoluciones de llamada , particularmente para controladores de eventos , como en JavaScript , donde se usan para interacciones con una página web dinámica .
Los cierres también se pueden usar en un estilo de paso de continuación para ocultar el estado . De este modo, las construcciones como objetos y estructuras de control se pueden implementar con cierres. En algunos lenguajes, puede ocurrir un cierre cuando una función se define dentro de otra función, y la función interna se refiere a variables locales de la función externa. En tiempo de ejecución , cuando se ejecuta la función externa, se forma un cierre, que consiste en el código de la función interna y las referencias (los upvalues) a cualquier variable de la función externa requerida por el cierre.
Funciones de primera clase
Los cierres suelen aparecer en lenguajes con funciones de primera clase; en otras palabras, dichos lenguajes permiten que las funciones se pasen como argumentos, se devuelvan desde llamadas a funciones, se enlacen a nombres de variables, etc., al igual que los tipos más simples como cadenas y enteros. Por ejemplo, considere la siguiente función de esquema :
; Devuelva una lista de todos los libros con al menos UMBRAL copias vendidas. ( Definir ( best-venta de libros umbral ) ( filtro ( lambda ( libro ) ( > = ( libro-venta de libros ) umbral )) libro-list ))
En este ejemplo, la expresión lambda (lambda (book) (>= (book-sales book) threshold))
aparece dentro de la función best-selling-books
. Cuando se evalúa la expresión lambda, Scheme crea un cierre que consta del código para la expresión lambda y una referencia a la threshold
variable, que es una variable libre dentro de la expresión lambda.
Luego, el cierre se pasa a la filter
función, que la llama repetidamente para determinar qué libros se agregarán a la lista de resultados y cuáles se descartarán. Debido a que el cierre en sí tiene una referencia a threshold
, puede usar esa variable cada vez que lo filter
llame. La función en filter
sí podría definirse en un archivo completamente separado.
Aquí está el mismo ejemplo reescrito en JavaScript , otro lenguaje popular con soporte para cierres:
// Devuelve una lista de todos los libros con al menos 'límite' de copias vendidas. función bestSellingBooks ( umbral ) { return bookList . filtro ( función ( libro ) { libro de devolución . ventas > = umbral ; } ); }
La function
palabra clave se usa aquí en lugar de lambda
, y un Array.filter
método [8] en lugar de una filter
función global , pero por lo demás, la estructura y el efecto del código son los mismos.
Una función puede crear un cierre y devolverlo, como en el siguiente ejemplo:
// Devuelve una función que se aproxime a la derivada de f // usando un intervalo de dx, que debería ser apropiadamente pequeño. función derivada ( f , dx ) { función de retorno ( x ) { retorno ( f ( x + dx ) - f ( x )) / dx ; }; }
Debido a que el cierre en este caso sobrevive a la ejecución de la función que lo crea, las variables f
y dx
viven después de que la función derivative
regrese, aunque la ejecución ha dejado su alcance y ya no son visibles. En lenguajes sin cierres, el tiempo de vida de una variable local automática coincide con la ejecución del marco de pila donde se declara esa variable. En los idiomas con cierres, las variables deben seguir existiendo siempre que los cierres existentes tengan referencias a ellos. Esto se implementa más comúnmente mediante algún tipo de recolección de basura .
Representación estatal
Un cierre se puede utilizar para asociar una función con un conjunto de variables " privadas ", que persisten durante varias invocaciones de la función. El alcance de la variable abarca solo la función cerrada, por lo que no se puede acceder a ella desde otro código de programa. Estas son análogas a las variables privadas en la programación orientada a objetos y, de hecho, los cierres son análogos a un tipo de objeto , específicamente objetos de función , con un solo método público (llamada de función) y posibles muchas variables privadas (las variables vinculadas).
En los lenguajes con estado, los cierres se pueden utilizar para implementar paradigmas para la representación del estado y el ocultamiento de información , ya que los valores superiores del cierre (sus variables cerradas) son de extensión indefinida , por lo que un valor establecido en una invocación permanece disponible en la siguiente. Los cierres utilizados de esta manera ya no tienen transparencia referencial y, por lo tanto, ya no son funciones puras ; sin embargo, se usan comúnmente en lenguajes funcionales impuros como Scheme .
Otros usos
Los cierres tienen muchos usos:
- Debido a que los cierres retrasan la evaluación, es decir, no "hacen" nada hasta que son llamados, pueden usarse para definir estructuras de control. Por ejemplo, todas las estructuras de control estándar de Smalltalk , incluidas las ramas (if / then / else) y los bucles (while y for), se definen utilizando objetos cuyos métodos aceptan cierres. Los usuarios también pueden definir fácilmente sus propias estructuras de control.
- En los lenguajes que implementan la asignación, se pueden producir múltiples funciones que se cierran sobre el mismo entorno, lo que les permite comunicarse de forma privada alterando ese entorno. En esquema:
( definir foo #f ) ( definir barra #f )( Dejar que (( secreta-mensaje "ninguna" )) ( set! Foo ( lambda ( msg ) ( set! Secreta mensaje MSG ))) ( set! Barra ( lambda () secreta-mensaje )))( pantalla ( barra )) ; imprime "ninguno" ( nueva línea ) ( foo "nos vemos en los muelles a la medianoche" ) ( pantalla ( barra )) ; imprime "nos vemos en los muelles a la medianoche"
- Los cierres se pueden utilizar para implementar sistemas de objetos . [9]
Nota: Algunos hablantes llaman clausura a cualquier estructura de datos que se vincule a un entorno léxico , pero el término generalmente se refiere específicamente a funciones.
Implementación y teoría
Los cierres se implementan normalmente con una estructura de datos especial que contiene un puntero al código de la función , además de una representación del entorno léxico de la función (es decir, el conjunto de variables disponibles) en el momento en que se creó el cierre. El entorno de referencia vincula los nombres no locales a las variables correspondientes en el entorno léxico en el momento en que se crea el cierre, extendiendo adicionalmente su vida útil al menos tanto como la vida útil del cierre en sí. Cuando se ingresa el cierre en un momento posterior, posiblemente con un entorno léxico diferente, la función se ejecuta con sus variables no locales refiriéndose a las capturadas por el cierre, no al entorno actual.
Una implementación de lenguaje no puede admitir cierres completos fácilmente si su modelo de memoria en tiempo de ejecución asigna todas las variables automáticas en una pila lineal . En tales lenguajes, las variables locales automáticas de una función se desasignan cuando la función regresa. Sin embargo, un cierre requiere que las variables libres a las que hace referencia sobrevivan a la ejecución de la función adjunta. Por lo tanto, esas variables deben asignarse para que persistan hasta que ya no se necesiten, generalmente a través de la asignación de pila , en lugar de en la pila, y su vida útil debe administrarse para que sobrevivan hasta que todos los cierres que hacen referencia a ellas ya no estén en uso.
Esto explica por qué, por lo general, los lenguajes que admiten cierres de forma nativa también utilizan la recolección de basura . Las alternativas son la gestión manual de la memoria de las variables no locales (asignando explícitamente en el montón y liberando cuando se hace), o, si se usa la asignación de pila, para que el lenguaje acepte que ciertos casos de uso conducirán a un comportamiento indefinido , debido a punteros colgantes a variables automáticas liberadas, como en expresiones lambda en C ++ 11 [10] o funciones anidadas en GNU C. [11] El problema funarg (o problema de "argumento funcional") describe la dificultad de implementar funciones como objetos de primera clase en una pila -Lenguaje de programación basado en C o C ++. De manera similar, en la versión 1 de D , se asume que el programador sabe qué hacer con los delegados y las variables locales automáticas, ya que sus referencias no serán válidas después de regresar de su alcance de definición (las variables locales automáticas están en la pila); esto aún permite muchas funciones útiles patrones funcionales, pero para casos complejos necesita una asignación de montón explícita para las variables. La versión 2 de D resolvió esto detectando qué variables deben almacenarse en el montón y realiza la asignación automática. Debido a que D usa recolección de basura, en ambas versiones, no hay necesidad de rastrear el uso de variables a medida que se pasan.
En lenguajes funcionales estrictos con datos inmutables ( por ejemplo, Erlang ), es muy fácil implementar la gestión automática de memoria (recolección de basura), ya que no hay ciclos posibles en las referencias de las variables. Por ejemplo, en Erlang, todos los argumentos y variables se asignan en el montón, pero las referencias a ellos se almacenan adicionalmente en la pila. Después de que una función regresa, las referencias siguen siendo válidas. La limpieza del montón se realiza mediante un recolector de basura incremental.
En ML, las variables locales tienen un alcance léxico y, por lo tanto, definen un modelo similar a una pila, pero dado que están vinculadas a valores y no a objetos, una implementación es libre de copiar estos valores en la estructura de datos del cierre de una manera que es invisible para el programador.
Scheme , que tiene un sistema de alcance léxico similar a ALGOL con variables dinámicas y recolección de basura, carece de un modelo de programación de pila y no sufre las limitaciones de los lenguajes basados en pila. Los cierres se expresan naturalmente en Scheme. La forma lambda encierra el código y las variables libres de su entorno persisten dentro del programa siempre que sea posible acceder a ellas, por lo que se pueden usar con tanta libertad como cualquier otra expresión de Scheme. [ cita requerida ]
Los cierres están estrechamente relacionados con Actores en el modelo Actor de cálculo concurrente donde los valores en el entorno léxico de la función se denominan conocidos . Un tema importante para los cierres en lenguajes de programación concurrentes es si las variables de un cierre se pueden actualizar y, de ser así, cómo se pueden sincronizar estas actualizaciones. Los actores proporcionan una solución. [12]
Los cierres están estrechamente relacionados con los objetos funcionales ; la transformación de la primera a la segunda se conoce como desfuncionalización o levantamiento lambda ; ver también conversión de cierre . [ cita requerida ]
Diferencias en semántica
Entorno léxico
Dado que los diferentes idiomas no siempre tienen una definición común del entorno léxico, sus definiciones de cierre también pueden variar. La definición minimalista comúnmente sostenida del entorno léxico lo define como un conjunto de todas las vinculaciones de variables en el alcance, y eso es también lo que los cierres en cualquier idioma tienen que capturar. Sin embargo, el significado de una vinculación variable también es diferente. En los lenguajes imperativos, las variables se unen a ubicaciones relativas en la memoria que pueden almacenar valores. Aunque la ubicación relativa de un enlace no cambia en tiempo de ejecución, el valor en la ubicación enlazada sí puede. En tales lenguajes, dado que el cierre captura el enlace, cualquier operación sobre la variable, ya sea que se realice desde el cierre o no, se realiza en la misma ubicación de memoria relativa. A esto se le suele llamar capturar la variable "por referencia". Aquí hay un ejemplo que ilustra el concepto en ECMAScript , que es uno de esos lenguajes:
// ECMAScript var f , g ; función foo () { var x ; f = función () { retorno ++ x ; }; g = función () { retorno - x ; }; x = 1 ; alert ( 'dentro de foo, llamar af ():' + f ()); } foo (); // 2 alert ( 'llamar a g ():' + g ()); // 1 (--x) alert ( 'llamar a g ():' + g ()); // 0 (--x) alert ( 'llamada af ():' + f ()); // 1 (++ x) alert ( 'llamada af ():' + f ()); // 2 (++ x)
La función foo
y los cierres a los que se refieren las variables f
y g
todos usan la misma ubicación de memoria relativa indicada por la variable local x
.
En algunos casos, el comportamiento anterior puede ser indeseable y es necesario vincular un cierre léxico diferente. Nuevamente en ECMAScript, esto se haría usando el Function.bind()
.
Ejemplo 1: referencia a una variable independiente
[13]
var module = { x : 42 , getX : function () { devuelve esto . x ; } } var unboundGetX = módulo . getX ; consola . log ( unboundGetX ()); // La función se invoca en el ámbito global // emite indefinido ya que 'x' no se especifica en el ámbito global.var boundGetX = unboundGetX . enlazar ( módulo ); // especificar el módulo de objeto como la consola de cierre . log ( boundGetX ()); // emite 42
Ejemplo 2: referencia accidental a una variable vinculada
Para este ejemplo, el comportamiento esperado sería que cada enlace debería emitir su ID al hacer clic; pero debido a que la variable 'e' está ligada al alcance anterior y se evalúa de manera perezosa al hacer clic, lo que realmente sucede es que cada evento al hacer clic emite la identificación del último elemento en 'elementos' ligado al final del ciclo for. [14]
var elementos = documento . getElementsByTagName ( 'a' ); // Incorrecto: e está vinculado a la función que contiene el bucle 'for', no al cierre de "handle" for ( var e de elementos ) { e . onclick = function handle () { alerta ( e . id );} }
Nuevamente, aquí la variable e
debería estar limitada por el alcance del bloque usando handle.bind(this)
o la let
palabra clave.
Por otro lado, muchos lenguajes funcionales, como ML , unen variables directamente a valores. En este caso, dado que no hay forma de cambiar el valor de la variable una vez que está vinculada, no es necesario compartir el estado entre cierres, solo usan los mismos valores. A esto a menudo se le llama capturar la variable "por valor". Las clases locales y anónimas de Java también entran en esta categoría: requieren que las variables locales capturadas sean final
, lo que también significa que no es necesario compartir el estado.
Algunos idiomas le permiten elegir entre capturar el valor de una variable o su ubicación. Por ejemplo, en C ++ 11, las variables capturadas se declaran con [&]
, lo que significa capturadas por referencia, o con [=]
, lo que significa capturadas por valor.
Otro subconjunto, los lenguajes funcionales perezosos como Haskell , vinculan variables a resultados de cálculos futuros en lugar de valores. Considere este ejemplo en Haskell:
- Haskell foo :: Fraccional a => a -> a -> ( a -> a ) foo x y = ( \ z -> z + r ) donde r = x / yf :: Fraccional a => a -> a f = foo 1 0principal = imprimir ( f 123 )
La unión de r
capturado por el cierre definido dentro de la función foo
es para el cálculo, (x / y)
que en este caso resulta en una división por cero. Sin embargo, dado que lo que se captura es el cálculo, y no el valor, el error solo se manifiesta cuando se invoca el cierre y, en realidad, intenta utilizar el enlace capturado.
Cierre dejando
Sin embargo, más diferencias se manifiestan en el comportamiento de otras construcciones alcance léxico, como por ejemplo return
, break
y continue
declaraciones. Tales construcciones pueden, en general, considerarse en términos de invocar una continuación de escape establecida por una declaración de control adjunta (en el caso de break
y continue
, tal interpretación requiere que las construcciones de bucle se consideren en términos de llamadas de función recursivas). En algunos lenguajes, como ECMAScript, se return
refiere a la continuación establecida por el cierre léxico más interno con respecto al enunciado; por lo tanto, return
un cierre dentro de un cierre transfiere el control al código que lo llamó. Sin embargo, en Smalltalk , el operador superficialmente similar ^
invoca la continuación de escape establecida para la invocación del método, ignorando las continuaciones de escape de cualquier cierre anidado intermedio. La continuación de escape de un cierre en particular solo se puede invocar en Smalltalk implícitamente al llegar al final del código del cierre. Los siguientes ejemplos en ECMAScript y Smalltalk resaltan la diferencia:
"Smalltalk" foo | xs | xs : = # ( 1 2 3 4 ) . xs hacen: [ : x | ^ x ] . ^ 0 barra Mostrar transcripción : ( self foo printString ) "imprime 1"
// Función ECMAScript foo () { var xs = [ 1 , 2 , 3 , 4 ]; xs . paraCada ( función ( x ) { retorno x ; }); return 0 ; } alerta ( foo ()); // imprime 0
Los fragmentos de código anteriores se comportarán de manera diferente porque el ^
operador Smalltalk y el return
operador JavaScript no son análogos. En el ejemplo de ECMAScript, return x
dejará el cierre interno para comenzar una nueva iteración del forEach
ciclo, mientras que en el ejemplo de Smalltalk, ^x
abortará el ciclo y regresará del método foo
.
Common Lisp proporciona una construcción que puede expresar cualquiera de las acciones anteriores: Lisp se (return-from foo x)
comporta como Smalltalk ^x
, mientras que Lisp se (return-from nil x)
comporta como JavaScript return x
. Por lo tanto, Smalltalk hace posible que una continuación de escape capturada sobreviva al grado en que se puede invocar con éxito. Considerar:
"Smalltalk" foo ^ [ : x | ^ x ] barra | f | f : = self foo . Valor f : 123 "¡error!"
Cuando foo
se invoca el cierre devuelto por el método , intenta devolver un valor de la invocación del foo
que creó el cierre. Dado que esa llamada ya ha regresado y el modelo de invocación del método Smalltalk no sigue la disciplina de la pila de espaguetis para facilitar múltiples devoluciones, esta operación da como resultado un error.
Algunos lenguajes, como Ruby , permiten al programador elegir la forma en que return
se captura. Un ejemplo en Ruby:
# Rubí# Cierre mediante un Proc def foo f = Proc . new { return "regresar de foo desde dentro del proc" } f . call # control deja foo aquí return "return from foo" fin# Cierre usando una barra def lambda f = lambda { return "return from lambda" } f . call # control no sale de la barra aquí return "return from bar" final pone foo # imprime "retorno de foo desde adentro proc" pone barra # imprime "retorno de la barra"
Ambos Proc.new
y lambda
en este ejemplo son formas de crear un cierre, pero la semántica de los cierres así creados es diferente con respecto a la return
declaración.
En Scheme , la definición y el alcance de la return
declaración de control es explícito (y solo se llama arbitrariamente 'retorno' por el bien del ejemplo). La siguiente es una traducción directa de la muestra de Ruby.
; Esquema ( definir llamada / cc llamada-con-continuación-actual )( define ( foo ) ( call / cc ( lambda ( return ) ( define ( f ) ( return "return from foo from inside proc" )) ( f ) ; el control deja foo aquí ( return "return from foo" ))))( define ( bar ) ( call / cc ( lambda ( return ) ( define ( f ) ( call / cc ( lambda ( return ) ( return "return from lambda" )))) ( f ) ; el control no deja la barra aquí ( return "volver de la barra" ))))( pantalla ( foo )) ; imprime "retorno de foo desde adentro proc" ( nueva línea ) ( display ( bar )) ; imprime "retorno de la barra"
Construcciones similares a cierres
Algunos lenguajes tienen características que simulan el comportamiento de los cierres. En lenguajes como Java, C ++, Objective-C, C #, VB.NET y D, estas características son el resultado del paradigma orientado a objetos del lenguaje.
Devolución de llamada (C)
Algunas bibliotecas de C admiten devoluciones de llamada . A veces, esto se implementa proporcionando dos valores al registrar la devolución de llamada con la biblioteca: un puntero de función y un void*
puntero separado a datos arbitrarios de la elección del usuario. Cuando la biblioteca ejecuta la función de devolución de llamada, pasa el puntero de datos. Esto permite que la devolución de llamada mantenga el estado y se refiera a la información capturada en el momento en que se registró en la biblioteca. El idioma es similar a los cierres en funcionalidad, pero no en sintaxis. El void*
puntero no es de tipo seguro, por lo que este modismo de C difiere de los cierres de tipo seguro en C #, Haskell o ML.
Las devoluciones de llamada se utilizan ampliamente en los kits de herramientas de GUI Widget para implementar la programación dirigida por eventos asociando funciones generales de widgets gráficos (menús, botones, casillas de verificación, controles deslizantes, controles giratorios, etc.) con funciones específicas de la aplicación que implementan el comportamiento específico deseado para la aplicación.
Función anidada y puntero de función (C)
Con una extensión gcc, se puede usar una función anidada y un puntero de función puede emular cierres, siempre que la función no salga del ámbito contenedor. El siguiente ejemplo no es válido porque adder
es una definición de nivel superior (dependiendo de la versión del compilador, podría producir un resultado correcto si se compila sin optimización, es decir, en -O0
):
#include typedef int ( * fn_int_to_int ) ( int ); // tipo de función int-> intfn_int_to_int sumador ( int número ) { int agregar ( int valor ) { valor de retorno + número ; } volver & agregar ; // El operador & es opcional aquí porque el nombre de una función en C es un puntero que apunta a sí mismo } int main ( vacío ) { fn_int_to_int add10 = sumador ( 10 ); printf ( "% d \ n " , agregar10 ( 1 )); return 0 ; }
Pero moverse adder
(y, opcionalmente, el typedef
) adentro lo main
hace válido:
#include int main ( vacío ) { typedef int ( * fn_int_to_int ) ( int ); // tipo de función int-> int fn_int_to_int sumador ( int número ) { int agregar ( int valor ) { valor de retorno + número ; } return add ; } fn_int_to_int add10 = sumador ( 10 ); printf ( "% d \ n " , agregar10 ( 1 )); return 0 ; }
Si se ejecuta, ahora se imprime 11
como se esperaba.
Clases locales y funciones lambda (Java)
Java permite definir clases dentro de métodos . Estos se llaman clases locales . Cuando estas clases no tienen nombre, se conocen como clases anónimas (o clases internas anónimas ). Una clase local (ya sea nombrada o anónima) puede referirse a nombres en clases que encierran léxicamente, o variables de solo lectura (marcadas como final
) en el método que encierra léxicamente.
class CalculationWindow extiende JFrame { resultado int volátil privado ; // ... public void calculateInSeparateThread ( URI final uri ) { // La expresión "new Runnable () {...}" es una clase anónima que implementa la interfaz 'Runnable'. new Thread ( new Runnable () { void run () { // Puede leer las variables locales finales: calcular ( uri ); // Puede acceder a los campos privados de la clase adjunta: resultado = resultado + 10 ; } } ). inicio (); } }
La captura de final
variables le permite capturar variables por valor. Incluso si la variable que desea capturar no es- final
, siempre puede copiarla a una final
variable temporal justo antes de la clase.
La captura de variables por referencia se puede emular utilizando una final
referencia a un contenedor mutable, por ejemplo, una matriz de un solo elemento. La clase local no podrá cambiar el valor de la referencia del contenedor en sí, pero podrá cambiar el contenido del contenedor.
Con la llegada de las expresiones lambda de Java 8, [15] el cierre hace que el código anterior se ejecute como:
class CalculationWindow extiende JFrame { resultado int volátil privado ; // ... public void calculateInSeparateThread ( URI final uri ) { // El código () -> {/ * código * /} es un cierre. new Thread (() -> { calcular ( uri ); resultado = resultado + 10 ; }). inicio (); } }
Las clases locales son uno de los tipos de clases internas que se declaran dentro del cuerpo de un método. Java también admite clases internas que se declaran como miembros no estáticos de una clase adjunta. [16] Normalmente se les llama "clases internas". [17] Estos se definen en el cuerpo de la clase adjunta y tienen acceso completo a las variables de instancia de la clase adjunta. Debido a su vinculación a estas variables de instancia, una clase interna solo puede instanciarse con una vinculación explícita a una instancia de la clase adjunta utilizando una sintaxis especial. [18]
clase pública EnclosingClass { / * Definir la clase interna * / clase pública InnerClass { public int incrementAndReturnCounter () { return counter ++ ; } } contador privado de int ; { contador = 0 ; } public int getCounter () { contador de retorno ; } public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); / * Crea una instancia de la clase interna, con enlace a la instancia * / EnclosingClass . InnerClass innerClassInstance = enclosingClassInstance . new InnerClass (); para ( int i = enclosingClassInstance . getCounter (); ( i = innerClassInstance . incrementAndReturnCounter ()) < 10 ; / * paso de incremento omitido * / ) { System . fuera . println ( i ); } } }
Tras la ejecución, imprimirá los números enteros del 0 al 9. Tenga cuidado de no confundir este tipo de clase con la clase anidada, que se declara de la misma manera con un uso acompañado del modificador "estático"; esos no tienen el efecto deseado, sino que son solo clases sin un enlace especial definido en una clase adjunta.
A partir de Java 8 , Java admite funciones como objetos de primera clase. Las expresiones lambda de esta forma se consideran de tipo, Function
siendo T el dominio y U el tipo de imagen. La expresión se puede llamar con su .apply(T t)
método, pero no con una llamada a un método estándar.
public static void main ( String [] args ) { Función < String , Integer > length = s -> s . longitud (); Sistema . fuera . println ( length . apply ( "¡Hola, mundo!" ) ); // Imprimirá 13. }
Bloques (C, C ++, Objective-C 2.0)
Apple introdujo bloques , una forma de cierre, como una extensión no estándar en C , C ++ , Objective-C 2.0 y en Mac OS X 10.6 "Snow Leopard" e iOS 4.0 . Apple puso su implementación a disposición de los compiladores GCC y clang.
Los punteros para bloquear y bloquear literales están marcados con ^
. Las variables locales normales se capturan por valor cuando se crea el bloque y son de solo lectura dentro del bloque. Las variables que se deben capturar por referencia están marcadas con __block
. Es posible que deban copiarse los bloques que deben persistir fuera del ámbito en el que se crearon. [19] [20]
typedef int ( ^ IntBlock ) ();IntBlock downCounter ( int start ) { __block int i = start ; return [[ ^ int () { return i - ; } copia ] liberación automática ]; }IntBlock f = contador descendente ( 5 ); NSLog ( @ "% d" , f ()); NSLog ( @ "% d" , f ()); NSLog ( @ "% d" , f ());
Delegados (C #, VB.NET, D)
Los métodos anónimos de C # y las expresiones lambda admiten el cierre:
var data = new [] { 1 , 2 , 3 , 4 }; var multiplicador = 2 ; var result = data . Seleccione ( x => x * multiplicador );
Visual Basic .NET , que tiene muchas características de lenguaje similares a las de C #, también admite expresiones lambda con cierres:
Atenuar datos = { 1 , 2 , 3 , 4 } Atenuar multiplicador = 2 Atenuar resultado = datos . Seleccione ( Función ( x ) x * multiplicador )
En D , los cierres son implementados por delegados, un puntero de función emparejado con un puntero de contexto (por ejemplo, una instancia de clase o un marco de pila en el montón en el caso de cierres).
auto test1 () { int a = 7 ; return delegate () { devuelve un + 3 ; }; // construcción de delegado anónimo }auto test2 () { int a = 20 ; int foo () { devuelve un + 5 ; } // retorno de la función interna & foo ; // otra forma de construir delegado } barra vacía () { auto dg = test1 (); dg (); // = 10 // ok, test1.a está en un cierre y todavía existe dg = test2 (); dg (); // = 25 // ok, test2.a está en un cierre y todavía existe }
D versión 1, tiene soporte de cierre limitado. Por ejemplo, el código anterior no funcionará correctamente, porque la variable a está en la pila, y después de regresar de test (), ya no es válido para usarlo (lo más probable es que llamar a foo a través de dg () devolverá un ' entero aleatorio). Esto se puede resolver asignando explícitamente la variable 'a' en el montón, o usando estructuras o clases para almacenar todas las variables cerradas necesarias y construir un delegado a partir de un método que implemente el mismo código. Los cierres se pueden pasar a otras funciones, siempre que solo se utilicen mientras los valores referenciados siguen siendo válidos (por ejemplo, llamar a otra función con un cierre como parámetro de devolución de llamada) y son útiles para escribir código de procesamiento de datos genérico, por lo que esta limitación , en la práctica, a menudo no es un problema.
Esta limitación se corrigió en la versión D 2: la variable 'a' se asignará automáticamente en el montón porque se usa en la función interna, y un delegado de esa función puede escapar del alcance actual (a través de la asignación a dg o return). Cualquier otra variable local (o argumento) a la que no hacen referencia los delegados o que solo hacen referencia a los delegados que no escapan del ámbito actual, permanece en la pila, que es más simple y rápida que la asignación de montón. Lo mismo es cierto para los métodos de clase de inner que hacen referencia a las variables de una función.
Objetos de función (C ++)
C ++ permite definir objetos de función mediante sobrecarga operator()
. Estos objetos se comportan como funciones en un lenguaje de programación funcional. Se pueden crear en tiempo de ejecución y pueden contener estado, pero no capturan implícitamente variables locales como lo hacen los cierres. A partir de la revisión de 2011 , el lenguaje C ++ también admite cierres, que son un tipo de objeto de función construido automáticamente a partir de una construcción de lenguaje especial llamada expresión lambda . Un cierre de C ++ puede capturar su contexto almacenando copias de las variables a las que se accede como miembros del objeto de cierre o por referencia. En el último caso, si el objeto de cierre escapa del alcance de un objeto referenciado, invocar sus operator()
causas comporta un comportamiento indefinido ya que los cierres de C ++ no extienden la vida útil de su contexto.
void foo ( string myname ) { int y ; vector < cadena > n ; // ... auto i = std :: find_if ( n . begin (), n . end (), // esta es la expresión lambda: [ & ] ( const string & s ) { return s ! = myname && s . tamaño () > y ; } ); // 'i' ahora es 'n.end ()' o apunta a la primera cadena en 'n' // que no es igual a 'myname' y cuya longitud es mayor que 'y' }
Agentes en línea (Eiffel)
Eiffel incluye agentes en línea que definen cierres. Un agente en línea es un objeto que representa una rutina, definido dando el código de la rutina en línea. Por ejemplo, en
ok_button . click_event . subscribe ( agente ( x , y : INTEGER ) do map . country_at_coordinates ( x , y ). display end )
el argumento de subscribe
es un agente, que representa un procedimiento con dos argumentos; el procedimiento encuentra el país en las coordenadas correspondientes y lo muestra. Todo el agente está "suscrito" al tipo de evento click_event
para un botón determinado, de modo que siempre que ocurra una instancia del tipo de evento en ese botón, porque un usuario ha hecho clic en el botón, el procedimiento se ejecutará con las coordenadas del mouse pasándose como argumentos a favor x
y y
.
La principal limitación de los agentes Eiffel, que los distingue de los cierres en otros idiomas, es que no pueden hacer referencia a variables locales del alcance adjunto. Esta decisión de diseño ayuda a evitar la ambigüedad cuando se habla de un valor de variable local en un cierre: ¿debería ser el último valor de la variable o el valor capturado cuando se crea el agente? Solo Current
(una referencia al objeto actual, análogo a this
en Java), sus características y argumentos del propio agente se puede acceder desde dentro del cuerpo del agente. Los valores de las variables locales externas se pueden pasar proporcionando operandos cerrados adicionales al agente.
C ++ Builder __closure palabra reservada
Embarcadero C ++ Builder proporciona la palabra de reserva __closure para proporcionar un puntero a un método con una sintaxis similar a un puntero de función. [21]
En C estándar, podría escribir una typedef para un puntero a un tipo de función usando la siguiente sintaxis:
typedef void ( * TMyFunctionPointer ) ( vacío );
De manera similar, puede declarar un typedef para un puntero a un método usando la siguiente sintaxis:
typedef void ( __closure * TMyMethodPointer ) ();
Ver también
- Función anónima
- Bloques (extensión de lenguaje C)
- Patrón de comando
- Continuación
- Zurra
- Problema de Funarg
- Cálculo lambda
- Evaluación perezosa
- Aplicación parcial
- Pila de espaguetis
- Cierre sintáctico
- Programación a nivel de valor
Notas
- ^ La función puede almacenarse como una referencia a una función, como un puntero de función .
- ^ Estos nombres se refieren con mayor frecuencia a valores, variables mutables o funciones, pero también pueden ser otras entidades como constantes, tipos, clases o etiquetas.
Referencias
- ^ Sussman y Steele. "Esquema: un intérprete de cálculo lambda extendido". "... una estructura de datos que contiene una expresión lambda y un entorno que se utilizará cuando esa expresión lambda se aplique a los argumentos". ( Wikisource )
- ^ David A. Turner (2012). "Un poco de historia de los lenguajes de programación funcionales" . Tendencias en programación funcional '12. La sección 2, nota 8, contiene la afirmación sobre las expresiones M.
- ^ PJ Landin (1964), La evaluación mecánica de expresiones
- ^ Joel Moses (junio de 1970), The Function of FUNCTION in LISP, or Why the FUNARG Problem Should Be Called the Environment Problem , hdl : 1721.1 / 5854 , AI Memo 199,
Una metáfora útil para la diferencia entre FUNCTION y QUOTE en LISP es Piense en QUOTE como una cobertura porosa o abierta de la función, ya que las variables libres escapan al entorno actual. FUNCIÓN actúa como una cubierta cerrada o no porosa (de ahí el término "cierre" utilizado por Landin). Por tanto, hablamos de expresiones Lambda "abiertas" (las funciones en LISP suelen ser expresiones Lambda) y expresiones Lambda "cerradas". [...] Mi interés por el problema del medio ambiente comenzó cuando Landin, que tenía un conocimiento profundo del problema, visitó el MIT durante 1966-1967. Entonces me di cuenta de la correspondencia entre las listas FUNARG que son los resultados de la evaluación de expresiones Lambda "cerradas" en LISP y los cierres Lambda de ISWIM .
- ^ Åke Wikström (1987). Programación funcional usando ML estándar . ISBN 0-13-331968-7.
La razón por la que se llama "cierre" es que una expresión que contiene variables libres se llama expresión "abierta" y, al asociarle las vinculaciones de sus variables libres, la cierra.
- ^ Gerald Jay Sussman y Guy L. Steele, Jr. (diciembre de 1975), Scheme: An Interpreter for the Extended Lambda Calculus , AI Memo 349
- ^ Abelson, Harold; Sussman, Gerald Jay; Sussman, Julie (1996). Estructura e interpretación de programas informáticos . Cambridge, MA: MIT Press. pag. 98–99. ISBN 0262510871.
- ^ "array.filter" . Centro de desarrolladores de Mozilla . 10 de enero de 2010 . Consultado el 9 de febrero de 2010 .
- ^ "Re: FP, OO y relaciones. ¿Alguien supera a los demás?" . 29 de diciembre de 1999. Archivado desde el original el 26 de diciembre de 2008 . Consultado el 23 de diciembre de 2008 .
- ^ Comité de estándares C ++ de expresiones lambda y cierres . 29 de febrero de 2008.
- ^ Manual de GCC, 6.4 Funciones anidadas , "Si intenta llamar a la función anidada a través de su dirección después de que la función contenedora salga, se desatará el infierno. Si intenta llamarla después de que salga un nivel de alcance contenedor, y si se refiere a alguna de las variables que ya no están dentro del alcance, puede tener suerte, pero no es prudente correr el riesgo. Sin embargo, si la función anidada no se refiere a nada que haya salido del alcance, debería estar seguro ".
- ^ Fundamentos de la semántica del actor Will Clinger. Tesis de Doctorado en Matemáticas del MIT. Junio de 1981.
- ^ "Function.prototype.bind ()" . Documentos web de MDN . Consultado el 20 de noviembre de 2018 .
- ^ "Cierres" . Documentos web de MDN . Consultado el 20 de noviembre de 2018 .
- ^ "Expresiones Lambda (los tutoriales de Java)" .
- ^ "Clases anidadas, internas, miembros y de nivel superior" .
- ^ "Ejemplo de clase interna (Tutoriales de Java> Aprendizaje del lenguaje Java> Clases y objetos)" .
- ^ "Clases anidadas (Tutoriales de Java> Aprendizaje del lenguaje Java> Clases y objetos)" .
- ^ Apple Inc. "Temas de programación de bloques" . Consultado el 8 de marzo de 2011 .
- ^ Joachim Bengtsson (7 de julio de 2010). "Programación con bloques C en dispositivos Apple" . Archivado desde el original el 25 de octubre de 2010 . Consultado el 18 de septiembre de 2010 .
- ^ La documentación completa se puede encontrar en http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure
enlaces externos
- "Papeles Lambda" originales : una serie clásica de artículos de Guy Steele y Gerald Sussman que discuten, entre otras cosas, la versatilidad de los cierres en el contexto de Scheme (donde aparecen como expresiones lambda ).
- Neal Gafter (28 de enero de 2007). "Una definición de cierres" .
- Gilad Bracha , Neal Gafter , James Gosling , Peter von der Ahé . "Cierres para el lenguaje de programación Java (v0.5)" .CS1 maint: varios nombres: lista de autores ( enlace )
- Cierres : un artículo sobre cierres en lenguajes imperativos tipados dinámicamente , de Martin Fowler .
- Métodos de cierre de la colección : un ejemplo de un dominio técnico donde el uso de cierres es conveniente, por Martin Fowler.