Cierre (programación informática)

Técnica para crear funciones de primera clase con alcance léxico

En lenguajes de programación , un cierre , también cierre léxico o cierre de función , es una técnica para implementar la vinculación de nombres con ámbito léxico en un lenguaje con funciones de primera clase . Operativamente , 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 envolvente) con el valor o referencia al que se vinculó 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 ámbito.

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 soportar funciones de primera clase con alcance léxico . [2]

Peter Landin definió el término cierre en 1964 como teniendo una parte de entorno y una parte de control como la utilizada por su máquina SECD para evaluar expresiones. [3] Joel Moses le atribuye a Landin la introducción del término cierre para referirse a una expresión lambda con enlaces abiertos (variables libres) que han sido cerrados por (o enlazados en) el entorno léxico, lo que resulta en una expresión cerrada o cierre. [4] [5] Este uso fue adoptado posteriormente por Sussman y Steele cuando definieron Scheme en 1975, [6] una variante de Lisp con ámbito léxico , y se generalizó.

Sussman y Abelson también utilizan el término "closs" 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 de poder también agregar estructuras de datos anidadas. Este uso del término proviene del uso matemático , en lugar del uso anterior en informática. Los autores consideran que esta superposición en la terminología es "desafortunada". [7]

Funciones anónimas

El término cierre se utiliza a menudo como sinónimo de función anónima , aunque estrictamente, una función anónima es un literal de función sin nombre, mientras que un cierre es una instancia de una función, un valor , cuyas variables no locales se han vinculado a valores o a ubicaciones de almacenamiento (dependiendo del lenguaje; consulte la sección del entorno léxico a continuación).

Por ejemplo, en el siguiente código 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.# Asignar cierres específicos a variables. a  =  f ( 1 ) b  =  h ( 1 )# Utilizando los cierres almacenados en variables. assert  a ( 5 )  ==  6 assert  b ( 5 )  ==  6# Usar cierres sin vincularlos primero a las variables. assert  f ( 1 )( 5 )  ==  6  # f(1) es el cierre. assert  h ( 1 )( 5 )  ==  6  # h(1) es el cierre.

Los valores de ay bson cierres, en ambos casos producidos al devolver una función anidada con una variable libre de la función que la encierra, de modo que la variable libre se vincula al valor del parámetro xde la función que la encierra. Los cierres en ay bson funcionalmente idénticos. La única diferencia en la implementación es que en el primer caso usamos una función anidada con un nombre, gmientras que en el segundo caso usamos una función anidada anónima (usando la palabra clave de Python lambdapara crear una función anónima). El nombre original, si lo hay, usado para definirlas es irrelevante.

Un cierre es un valor como cualquier otro valor. No necesita asignarse a una variable y, en cambio, puede usarse 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 en sí mismas clausuras: tienen una variable libre que aún no está vinculada. Solo cuando la función que las contiene se evalúa con un valor para el parámetro, se vincula la variable libre de la función anidada, lo que crea una clausura que luego se devuelve desde la función que las contiene.

Por último, un cierre solo se distingue de una función con variables libres cuando está fuera del ámbito 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 (no se puede distinguir la vinculación estática y dinámica porque los nombres se resuelven en los mismos valores). Por ejemplo, en el programa siguiente, las funciones con una variable libre x(vinculada a la variable no local xcon ámbito global) se ejecutan en el mismo entorno en el que xse define, por lo que no importa si se trata de cierres:

x  =  1 números  =  [ 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 fse puede ver que es un cierre porque xen el cuerpo de festá vinculado al xen el espacio de nombres global, no al xlocal to g:

x  =  0def  f ( y ):  devuelve  x  +  ydef  g ( z ):  x  =  1  # sombras locales x global x  return  f ( z )g ( 1 )  # evalúa 1, no 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 de función; si las funciones con variables libres son de primera clase, entonces devolver una crea un cierre. Esto incluye lenguajes de programación funcional como Lisp y ML , y muchos lenguajes modernos multiparadigma, como Julia , Python y Rust . Los cierres también se usan a menudo con devoluciones de llamadas , 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 utilizar en un estilo de paso de continuación para ocultar el estado . Por lo tanto, las construcciones como los objetos y las estructuras de control se pueden implementar con cierres. En algunos lenguajes, un cierre puede ocurrir cuando se define una función dentro de otra función y la función interna hace referencia 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 valores ascendentes) 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 de llamadas a funciones, se vinculen a nombres de variables, etc., al igual que los tipos más simples, como cadenas y números enteros. Por ejemplo, considere la siguiente función de Scheme :

; Devuelve una lista de todos los libros con al menos UMBRAL de copias vendidas. ( define ( best-selling-books umbral ) ( filter ( lambda ( book ) ( >= ( book-sales book ) umbral )) lista-de-libros ))          

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 de la expresión lambda y una referencia a la thresholdvariable, que es una variable libre dentro de la expresión lambda.

El cierre se pasa entonces a la filterfunción, que lo invoca repetidamente para determinar qué libros se deben agregar a la lista de resultados y cuáles se deben descartar. Como el cierre tiene una referencia a threshold, puede usar esa variable cada vez que filterlo invoca. La función filterse puede definir en un archivo independiente.

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 'umbral' de copias vendidas . function bestSellingBooks ( umbral ) { return bookList.filter ( book = > book.sales > = umbral ) ; }        

El operador de flecha =>se utiliza para definir una expresión de función de flecha y un Array.filtermétodo [8] en lugar de una filterfunció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 este ejemplo:

// Devuelve una función que se aproxima a la derivada de f // usando un intervalo de dx, que debe ser apropiadamente pequeño. function derived ( f , dx ) { return x = > ( 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 creó, las variables fy dxsiguen vivas después de que la función derivativeretorna, incluso aunque la ejecución haya dejado su alcance y ya no sean visibles. En lenguajes sin cierres, la duración de una variable local automática coincide con la ejecución del marco de pila donde se declara esa variable. En lenguajes con cierres, las variables deben seguir existiendo mientras cualquier cierre existente tenga referencias a ellas. Esto se implementa más comúnmente usando alguna forma 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 a lo largo de 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 similares a los objetos de función con estado (o funtores) con un único método de operador de llamada.

En lenguajes con estado, los cierres pueden usarse para implementar paradigmas de representación de estado y ocultamiento de información , ya que los valores de 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 se los llama), se pueden utilizar 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 cierran sobre el mismo entorno, lo que les permite comunicarse de forma privada alterando ese entorno. En Scheme:
( define foo #f ) ( define bar #f )    ( let (( mensaje-secreto "none" )) ( set! foo ( lambda ( msg ) ( set! mensaje-secreto msg ))) ( set! bar ( lambda () mensaje-secreto )))              ( display ( bar )) ; imprime "none" ( nueva línea ) ( foo "encuéntrame en los muelles a medianoche" ) ( display ( bar )) ; imprime "encuéntrame en los muelles a medianoche"     
  • Los cierres se pueden utilizar para implementar sistemas de objetos . [9]

Nota: Algunos hablantes llaman cierre a cualquier estructura de datos que vincule 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 además su duración al menos tanto como la duración del cierre. Cuando se ingresa al cierre en un momento posterior, posiblemente con un entorno léxico diferente, la función se ejecuta con sus variables no locales haciendo referencia a las capturadas por el cierre, no al entorno actual.

Una implementación de lenguaje no puede soportar fácilmente cierres completos 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 retorna. Sin embargo, un cierre requiere que las variables libres a las que hace referencia sobrevivan a la ejecución de la función que las contiene. Por lo tanto, esas variables deben asignarse de manera que persistan hasta que ya no sean necesarias, generalmente a través de la asignación de montón , en lugar de en la pila, y su vida útil debe administrarse de manera que sobrevivan hasta que todos los cierres que hacen referencia a ellas ya no estén en uso.

Esto explica por qué, típicamente, los lenguajes que soportan cierres de forma nativa también usan recolección de basura . Las alternativas son la administración manual de memoria de variables no locales (asignando explícitamente en el montón y liberando cuando se hace), o, si se usa asignación de pila, 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 del "argumento funcional") describe la dificultad de implementar funciones como objetos de primera clase en un lenguaje de programación basado en pila como 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 serán inválidas después de regresar de su ámbito de definición (las variables locales automáticas están en la pila); esto aún permite muchos patrones funcionales útiles, pero para casos complejos necesita una asignación explícita en el montón para las variables. La versión 2 de D resolvió este problema detectando qué variables deben almacenarse en el montón y realiza la asignación automática. Debido a que D utiliza recolección de elementos no utilizados, en ambas versiones no es necesario realizar un seguimiento del uso de las variables a medida que se pasan.

En lenguajes estrictamente funcionales con datos inmutables ( p. ej. 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 retorna, 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 como 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 en pila y no sufre las limitaciones de los lenguajes basados ​​en pila. Los cierres se expresan de forma natural en Scheme. La forma lambda encierra el código y las variables libres de su entorno persisten dentro del programa mientras sea posible acceder a ellas, por lo que pueden usarse con tanta libertad como cualquier otra expresión de Scheme. [ cita requerida ]

Los cierres están estrechamente relacionados con los actores en el modelo de Actor de computación concurrente, donde los valores en el entorno léxico de la función se denominan conocidos . Una cuestión importante para los cierres en lenguajes de programación concurrente es si las variables en 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 de función ; la transformación de los primeros a los segundos se conoce como desfuncionalización o elevación lambda ; véase también conversión de cierres . [ cita requerida ]

Diferencias en semántica

Entorno léxico

Como los distintos lenguajes 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 aceptada del entorno léxico lo define como un conjunto de todos los enlaces de variables en el ámbito, y eso es también lo que los cierres en cualquier lenguaje tienen que capturar. Sin embargo, el significado de un enlace de variable también difiere. En lenguajes imperativos, las variables se enlazan 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 en la variable, ya sea realizada desde el cierre o no, se realiza en la misma ubicación relativa de la memoria. Esto a menudo se llama capturar la variable "por referencia". Aquí hay un ejemplo que ilustra el concepto en ECMAScript , que es uno de esos lenguajes:

// Javascript var f , g ; función foo () { var x ; f = función () { return ++ x ; }; g = función () { return -- x ; }; x = 1 ; alerta ( 'dentro de foo, llama a f(): ' + f ()); } foo (); // 2 alerta ( 'llamada a g(): ' + g ()); // 1 (--x) alerta ( 'llamada a g(): ' + g ()); // 0 (--x) alerta ( 'llamada a f(): ' + f ()); // 1 (++x) alerta ( 'llamada a f(): ' + f ()); // 2 (++x)                                       

La función fooy los cierres a los que hacen referencia las variables fy gtodos utilizan la misma ubicación de memoria relativa indicada por la variable local x.

En algunos casos, el comportamiento anterior puede no ser deseable y es necesario vincular un cierre léxico diferente. Nuevamente, en ECMAScript, esto se haría utilizando el método Function.bind().

Ejemplo 1: Referencia a una variable no vinculada

[13]

var module = { x : 42 , getX : function () { return this . x ; } } var unboundGetX = module . getX ; console . log ( unboundGetX ()); // La función se invoca en el ámbito global // emite undefined ya que 'x' no está especificado en el ámbito global.              var boundGetX = unboundGetX . bind ( module ); // especifica el objeto module como el cierre console . log ( boundGetX ()); // emite 42     

Ejemplo 2: Referencia accidental a una variable enlazada

Para este ejemplo, el comportamiento esperado sería que cada enlace emitiera su id al hacer clic; pero debido a que la variable 'e' está vinculada al alcance anterior y se evalúa de forma diferida al hacer clic, lo que realmente sucede es que cada evento al hacer clic emite el id del último elemento en 'elementos' vinculado al final del bucle for. [14]

var elements = document . getElementsByTagName ( 'a' ); // Incorrecto: e está vinculado a la función que contiene el bucle 'for', no al cierre de "handle" for ( var e of elements ) { e . onclick = function handle () { alert ( e . id ); } }                  

Nuevamente aquí la variable edeberá estar limitada por el alcance del bloque usando handle.bind(this)la letpalabra clave.

Por otro lado, muchos lenguajes funcionales, como ML , vinculan variables directamente a valores. En este caso, dado que no hay forma de cambiar el valor de la variable una vez vinculada, no hay necesidad de compartir el estado entre cierres; simplemente usan los mismos valores. Esto a menudo se denomina 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 hay necesidad de compartir el estado.

Algunos lenguajes 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 que se capturan por referencia, o con [=], lo que significa que se capturan por valor.

Otro subconjunto, los lenguajes funcionales perezosos como Haskell , vinculan las variables a los resultados de cálculos futuros en lugar de a los valores. Considere este ejemplo en Haskell:

-- Haskell foo :: Fraccionario a => a -> a -> ( a -> a ) foo x y = ( \ z -> z + r ) donde r = x / y                         f :: Fraccionario a => a -> a f = foo 1 0           principal = imprimir ( f 123 )    

La vinculación de rlo capturado por el cierre definido dentro de la función fooes con 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 luego se intenta utilizar la vinculación capturada.

Cierre dejando

Sin embargo, se manifiestan más diferencias en el comportamiento de otras construcciones con alcance léxico, como las declaraciones return, breaky continue. Dichas construcciones pueden, en general, considerarse en términos de invocar una continuación de escape establecida por una declaración de control envolvente (en el caso de breaky continue, dicha interpretación requiere que las construcciones de bucle se consideren en términos de llamadas de función recursivas). En algunos lenguajes, como ECMAScript, returnse refiere a la continuación establecida por el cierre léxicamente más interno con respecto a la declaración; por lo tanto, a returndentro 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 particular solo se puede invocar en Smalltalk de manera implícita al llegar al final del código del cierre. Estos ejemplos en ECMAScript y Smalltalk resaltan la diferencia:

"Smalltalk" foo  | xs |  xs  :=  #( 1  2  3  4 ) .  xs  hacer: [ : x  |  ^ x ] .  ^ 0 bar  Transcripción  mostrar: ( self  foo  printString ) "imprime 1"
// Función ECMAScript foo () { var xs = [ 1 , 2 , 3 , 4 ]; xs . forEach ( function ( x ) { return x ; }); return 0 ; } alert ( foo ()); // imprime 0                  

Los fragmentos de código anteriores se comportarán de manera diferente porque el ^operador Smalltalk y el operador JavaScript returnno son análogos. En el ejemplo de ECMAScript, return xabandonará el cierre interno para comenzar una nueva iteración del forEachbucle, mientras que en el ejemplo de Smalltalk, ^xabortará el bucle y regresará del método foo.

Common Lisp ofrece una construcción que puede expresar cualquiera de las acciones anteriores: Lisp (return-from foo x)se comporta como Smalltalk ^x , mientras que Lisp (return-from nil x)se comporta como JavaScript return x . Por lo tanto, Smalltalk hace posible que una continuación de escape capturada sobreviva al tiempo en que se la puede invocar con éxito. Considere lo siguiente:

"Smalltalk" foo  ^ [ : x  |  ^ x ] bar  |  f  |  f  :=  self  foo .  f  valor:  123  "¡error!"

Cuando se invoca el cierre devuelto por el método foo, este intenta devolver un valor de la invocación del foométodo que creó el cierre. Dado que esa llamada ya ha retornado y el modelo de invocación del método Smalltalk no sigue la disciplina de pila de espagueti para facilitar múltiples devoluciones, esta operación genera un error.

Algunos lenguajes, como Ruby , permiten al programador elegir la forma returnde captura. Un ejemplo en Ruby:

#Rubí# Cierre usando un Proc def foo f = Proc . new { return "regresa de foo desde dentro de proc" } f . call # el control deja foo aquí return "regresa de foo" end            # Cierre usando una lambda def bar f = lambda { return "return from lambda" } f . call # el control no sale de bar aquí return "return from bar" end            pone foo # imprime "regresa de foo desde dentro de proc" pone bar # imprime "regresa de bar"    

En este ejemplo, tanto "como" Proc.newson lambdaformas de crear un cierre, pero la semántica de los cierres así creados es diferente con respecto a la returndeclaración.

En Scheme , la definición y el alcance de la returndeclaración de control son explícitos (y solo se los denomina arbitrariamente "return" para fines del ejemplo). La siguiente es una traducción directa del ejemplo de Ruby.

; Esquema ( definir llamada/cc llamada-con-continuación-actual )  ( define ( foo ) ( call/cc ( lambda ( return ) ( define ( f ) ( return "regresa de foo desde dentro de proc" )) ( f ) ; el control deja foo aquí ( return "regresa de foo" ))))            ( define ( bar ) ( call/cc ( lambda ( return ) ( define ( f ) ( call/cc ( lambda ( return ) ( return "return de lambda" )))) ( f ) ; el control no sale de bar aquí ( return "return de bar" ))))               ( display ( foo )) ; imprime "regresa de foo desde dentro de proc" ( nueva línea ) ( display ( bar )) ; imprime "regresa de bar"    

Construcciones de tipo cierre

Algunos lenguajes tienen características que simulan el comportamiento de los cierres. En lenguajes como C++ , C# , D , Java , Objective-C y Visual Basic (.NET) (VB.NET), estas características son el resultado del paradigma orientado a objetos del lenguaje.

Devoluciones de llamadas (C)

Algunas bibliotecas de C admiten devoluciones de llamadas . Esto a veces 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 elegidos por el 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 haga referencia a la información capturada en el momento en que se registró con la biblioteca. El modismo es similar a los cierres en cuanto a funcionalidad, pero no en cuanto a sintaxis. El void*puntero no es seguro para los tipos , por lo que este modismo de C difiere de los cierres seguros para los tipos en C#, Haskell o ML.

Las devoluciones de llamadas se utilizan ampliamente en los kits de herramientas de widgets de interfaz gráfica de usuario (GUI) para implementar una programación basada en eventos mediante la asociación de funciones generales de widgets gráficos (menús, botones, casillas de verificación, controles deslizantes, ruletas, etc.) con funciones específicas de la aplicación que implementan el comportamiento deseado específico para la aplicación.

Función anidada y puntero de función (C)

Con una extensión de GNU Compiler Collection (GCC), se puede utilizar una función anidada [15]adder 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 es una definición de nivel superior (según la versión del compilador, podría producir un resultado correcto si se compila sin optimización, es decir, en -O0):

#incluir <stdio.h> typedef int ( * fn_int_to_int )( int ); // tipo de función int->int   fn_int_to_int adder ( int número ) { int add ( int valor ) { return valor + número ; } return & add ; // El operador & es opcional aquí porque el nombre de una función en C es un puntero que apunta a sí mismo }                int main ( void ) { fn_int_to_int add10 = sumador ( 10 ); printf ( "%d \n " , add10 ( 1 )); devolver 0 ; }          

Pero mover adder(y, opcionalmente, el typedef) hacia adentro mainlo hace válido:

#incluir <stdio.h> int main ( void ) { typedef int ( * fn_int_to_int )( int ); // tipo de función int->int fn_int_to_int adder ( int number ) { int add ( int value ) { return value + number ; } return add ; } fn_int_to_int add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); return 0 ; }                                 

Si se ejecuta esto ahora se imprime 11como se esperaba.

Clases locales y funciones lambda (Java)

Java permite definir clases dentro de métodos . Estas se denominan clases locales . Cuando estas clases no tienen nombre, se las conoce como clases anónimas (o clases internas anónimas ). Una clase local (con nombre o anónima) puede hacer referencia a nombres en clases que las encierran léxicamente o a variables de solo lectura (marcadas como final) en el método que las encierra léxicamente.

clase  CalculationWindow extiende JFrame { private volcanic int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // La expresión "new Runnable() { ... }" es una clase anónima que implementa la interfaz 'Runnable'. new Thread ( new Runnable () { void run () { // Puede leer variables locales finales: calculate ( uri ); // Puede acceder a campos privados de la clase envolvente: result = result + 10 ; } } ). start (); } }                                   

La captura de finalvariables permite capturar variables por valor. Incluso si la variable a capturar no es final, siempre se puede copiar a una finalvariable temporal justo antes de la clase.

La captura de variables por referencia se puede emular utilizando una finalreferencia a un contenedor mutable, por ejemplo, una matriz de un elemento. La clase local no podrá cambiar el valor de la referencia del contenedor, pero sí podrá cambiar el contenido del contenedor.

Con la llegada de las expresiones lambda de Java 8, [16] el cierre hace que el código anterior se ejecute como:

clase  CalculationWindow extiende JFrame { private volcanic int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // El código () -> { /* código */ } es un cierre. new Thread (() -> { calculate ( uri ); result = result + 10 ; }). start (); } }                           

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 envolvente. [17] Normalmente se las denomina simplemente "clases internas". [18] Se definen en el cuerpo de la clase envolvente y tienen acceso total a las variables de instancia de la clase envolvente. Debido a su vinculación con estas variables de instancia, una clase interna solo se puede instanciar con una vinculación explícita a una instancia de la clase envolvente utilizando una sintaxis especial. [19]

clase pública EnclosingClass { /* Define la clase interna */ clase pública InnerClass { int public incrementAndReturnCounter () { return counter ++ ; } }                 int privado contador ; { contador = 0 ; }        public int getCounter () { devolver contador ; }       public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); /* Crear 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 . out . println ( i ); } } }              

Al ejecutarse, esto 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 "static"; estas no tienen el efecto deseado, sino que son simplemente clases sin un enlace especial definido en una clase envolvente.

A partir de Java 8 , Java admite funciones como objetos de primera clase. Las expresiones Lambda de esta forma se consideran de tipo Function<T,U>, 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 de método estándar.

public static void main ( String [] args ) { Función < String , Integer > longitud = s -> s . length ();             Sistema . out . println ( length . apply ( "¡Hola, mundo!" ) ); // Imprimirá 13. }   

Bloques (C, C++, Objective-C 2.0)

Apple introdujo los 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 a bloques y literales de bloques 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 capturarán por referencia están marcadas con __block. Es posible que sea necesario copiar los bloques que deben persistir fuera del ámbito en el que se crearon. [20] [21]

tipo definido int ( ^ IntBlock ) ();  IntBlock downCounter ( int inicio ) { __block int i = inicio ; return [[ ^ int () { return i -- ; } copia ] autorelease ]; }                 IntBlock f = contadorAbajo ( 5 ); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ());      

Delegados (C#, VB.NET, D)

Los métodos anónimos y las expresiones lambda de C# admiten el cierre:

var datos = nuevo [] { 1 , 2 , 3 , 4 }; multiplicador var = 2 ; var resultado = datos . 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:

Dim data = { 1 , 2 , 3 , 4 } Dim multiplicador = 2 Dim resultado = data . Select ( Función ( x ) x * multiplicador )               

En D , los cierres se implementan mediante 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 los cierres).

auto test1 () { int a = 7 ; return delegate () { return a + 3 ; }; // construcción de delegado anónimo }               auto test2 () { int a = 20 ; int foo () { return a + 5 ; } // función interna return & foo ; // otra forma de construir un delegado }                  void bar () { 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 }    

La versión 1 de D tiene un 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 usarla (lo más probable es que al llamar a foo a través de dg(), se devuelva un entero "aleatorio"). Esto se puede resolver asignando explícitamente la variable "a" en el montón, o utilizando 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 a los que se hace referencia sigan siendo válidos (por ejemplo, llamando 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 solucionó en la versión 2 de D: 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 ámbito actual (a través de la asignación a dg o return). Cualquier otra variable local (o argumento) a la que no hagan referencia los delegados o que solo hagan referencia los delegados que no escapan del ámbito actual, permanecerá en la pila, lo que es más simple y rápido que la asignación en el montón. Lo mismo se aplica a los métodos de clase internos que hacen referencia a las variables de una función.

Objetos de función (C++)

C++ permite definir objetos de función mediante la sobrecarga operator(). Estos objetos se comportan de forma similar a las funciones en un lenguaje de programación funcional. Pueden crearse 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 ya sea 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 ámbito de un objeto referenciado, invocarlo operator()provoca un comportamiento indefinido ya que los cierres de C++ no extienden la vida útil de su contexto.

void foo ( string myname ) { int y ; vector < string > n ; // ... auto i = std :: find_if ( n.begin ( ) ), n.end ( ) , // esta es la expresión lambda: [ & ] (const string &s) { return s ! = myname && s.size ( ) > 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, definida al proporcionar el código de la rutina en línea. Por ejemplo, en

botón_ok .click_event .subscribe ( agente ( x , y : INTEGER ) hacer mapa .país_en_coordenadas ( x , y ) .mostrar fin )       

El argumento subscribees 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_eventpara un botón determinado, de modo que siempre que se produzca 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 ratón que se pasan como argumentos para xy y.

La principal limitación de los agentes Eiffel, que los distingue de los cierres en otros lenguajes, es que no pueden hacer referencia a variables locales desde el ámbito que los encierra. Esta decisión de diseño ayuda a evitar ambigüedades 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 se puede acceder Currenta (una referencia al objeto actual, de manera análoga a thisJava), sus características y argumentos del agente desde dentro del cuerpo del agente. Los valores de las variables locales externas se pueden pasar proporcionando operandos cerrados adicionales al agente.

Palabra reservada __closure de C++Builder

Embarcadero C++Builder proporciona la palabra reservada __closurepara proporcionar un puntero a un método con una sintaxis similar a la de un puntero de función. [22]

El estándar C permite escribir un typedef para un puntero a un tipo de función utilizando la siguiente sintaxis:

typedef void ( * TMyFunctionPointer )( vacío );    

De manera similar, se puede declarar un typedef para un puntero a un método utilizando esta sintaxis:

typedef void ( __closure * TMyMethodPointer )();   

Véase también

Notas

  1. ^ La función puede almacenarse como una referencia a una función, como un puntero de función .
  2. ^ Estos nombres generalmente se refieren a valores, variables mutables o funciones, pero también pueden ser otras entidades como constantes, tipos, clases o etiquetas.

Referencias

  1. ^ Sussman y Steele. "Scheme: Un intérprete para el 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)
  2. ^ Turner, David A. (2012). "Some History of Functional Programming Languages" (PDF) . International Symposium on Trends in Functional Programming . Lecture Notes in Computer Science. Vol. 7829. Springer. pp. 1–20. Véase 12 §2, nota 8 para la afirmación sobre las M-expresiones. doi :10.1007/978-3-642-40447-4_1. ISBN 978-3-642-40447-4.
  3. ^ Landin, PJ (enero de 1964). «La evaluación mecánica de las expresiones» (PDF) . La revista informática . 6 (4): 308–320. doi : 10.1093/comjnl/6.4.308.
  4. ^ Moses, Joel (junio de 1970). "La función de FUNCTION en LISP, o por qué el problema de FUNARG debería llamarse el problema del entorno". Boletín ACM SIGSAM (15): 13–27. doi :10.1145/1093410.1093411. hdl :1721.1/5854. S2CID  17514262. AI Memo 199. Una metáfora útil para la diferencia entre FUNCTION y QUOTE en LISP es pensar en QUOTE como una cubierta porosa o abierta de la función, ya que las variables libres escapan al entorno actual. FUNCTION actúa como una cubierta cerrada o no porosa (de ahí el término "cierre" utilizado por Landin). Por lo tanto, hablamos de expresiones Lambda "abiertas" (las funciones en LISP suelen ser expresiones Lambda) y expresiones Lambda "cerradas". [...] Mi interés por el problema medioambiental comenzó cuando Landin, que tenía un profundo conocimiento del problema, visitó el MIT en 1966-67. 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 Lambda Closures de ISWIM .
  5. ^ Wikström, Åke (1987). Programación funcional utilizando ML estándar . Prentice Hall. ISBN 0-13-331968-7La 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 los enlaces de sus variables libres, la cierra .
  6. ^ Sussman, Gerald Jay ; Steele, Guy L. Jr. (diciembre de 1975). Scheme: An Interpreter for the Extended Lambda Calculus (Informe). AI Memo 349.
  7. ^ Abelson, Harold ; Sussman, Gerald Jay ; Sussman, Julie (1996). Estructura e interpretación de programas informáticos. MIT Press. págs. 98-99. ISBN 0-262-51087-1.
  8. ^ "array.filter". Centro de desarrolladores de Mozilla . 10 de enero de 2010. Consultado el 9 de febrero de 2010 .
  9. ^ "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 .
  10. ^ Comité de estándares de C++ sobre expresiones y cierres Lambda . 29 de febrero de 2008.
  11. ^ "6.4 Funciones anidadas". Manual de GCC . Si intenta llamar a la función anidada a través de su dirección después de que la función que la contiene salga, se desatará el infierno. Si intenta llamarla después de que salga un nivel de ámbito de contención, y si hace referencia a algunas de las variables que ya no están dentro del ámbito, puede tener suerte, pero no es prudente correr el riesgo. Sin embargo, si la función anidada no hace referencia a nada que haya salido del ámbito, debería estar seguro.
  12. ^ Fundamentos de la semántica de actores Will Clinger. Tesis doctoral en matemáticas del MIT. Junio ​​de 1981.
  13. ^ "Function.prototype.bind()". Documentos web de MDN . Consultado el 20 de noviembre de 2018 .
  14. ^ "Cierres". MDN Web Docs . Consultado el 20 de noviembre de 2018 .
  15. ^ "Funciones anidadas".
  16. ^ "Expresiones Lambda". Tutoriales de Java .
  17. ^ "Clases anidadas, internas, de miembro y de nivel superior". Blog de Oracle de Joseph D. Darcy . Julio de 2007. Archivado desde el original el 31 de agosto de 2016.
  18. ^ "Ejemplo de clase interna". Tutoriales de Java: Aprendiendo el lenguaje Java: Clases y objetos .
  19. ^ "Clases anidadas". Tutoriales de Java: Aprendiendo el lenguaje Java: Clases y objetos .
  20. ^ "Temas de programación de bloques". Apple Inc. 8 de marzo de 2011. Consultado el 8 de marzo de 2011 .
  21. ^ Bengtsson, Joachim (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 .
  22. ^ La documentación completa se puede encontrar en http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure
  • "Lambda Papers" originales: una serie clásica de artículos de Guy L. Steele Jr. y Gerald Jay Sussman que analizan, entre otras cosas, la versatilidad de los cierres en el contexto de Scheme (donde aparecen como expresiones lambda ).
  • Gafter, Neal (28 de enero de 2007). "Una definición de cierres".
  • Bracha, Gilad ; Gafter, Neal; Gosling, James ; von der Ahé, Peter. "Cierres para el lenguaje de programación Java (v0.5)".
  • Cierres: Un artículo sobre cierres en lenguajes imperativos tipados dinámicamente , por Martin Fowler .
  • Métodos de cierre de colecciones: un ejemplo de un dominio técnico donde es conveniente utilizar cierres, por Martin Fowler.
Retrieved from "https://en.wikipedia.org/w/index.php?title=Closure_(computer_programming)&oldid=1246240241"