Articulo de referencia

Cierre (programación informática)

En los lenguajes de programación , un cierre , también llamado cierre léxico o cierre de función , es una técnica para implementar la vinculación de nombres con ámbito léxico en...

En los lenguajes de programación , un cierre , también llamado 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 . Operacionalmente , un cierre es un registro que almacena una función [ a ] junto con un entorno. [ 1 ] El entorno es una asignación que asocia cada variable libre de la función (variables que se usan localmente, pero definidas en un ámbito que la contiene) con el valor o la referencia a la 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 de sus valores o referencias que contiene el cierre, 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 lambda y se implementó por primera vez de forma completa en 1970 como una característica del lenguaje de programación PAL para admitir funciones de primera clase con ámbito léxico . [ 2 ]

Peter Landin definió el término cierre en 1964 como algo que tiene una parte de entorno y una parte de control , tal como lo usaba 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 ligados 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 utilizaron 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 de poder agregar también estructuras de datos anidadas. Este uso del término proviene de las matemáticas , en lugar de su uso previo en informática. Los autores consideran que esta superposición terminológica es «desafortunada». [ 7 ]

Funciones anónimas

El término cierre se usa a menudo como sinónimo de función anónima , aunque, estrictamente hablando, una función anónima es una función literal 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 ubicaciones de almacenamiento (dependiendo del lenguaje; véase la sección del entorno léxico más adelante).

Por ejemplo, en el siguiente código Python :

def f ( x : int ) -> Callable [[ int ], int ]: def g ( y : int ) -> int : return x + y return g # Devuelve un cierre.def h ( x : int ) -> Callable [[ int ], int ]: return lambda y : x + y # Devuelve un cierre.# Asignación de cierres específicos a las variables. a : Callable [[ int ], int ] = f ( 1 ) b : Callable [[ int ], int ] = h ( 1 )# Usando los cierres almacenados en variables. assert a ( 5 ) == 6 assert b ( 5 ) == 6# Usando cierres sin vincularlos primero a 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 contenedora, de modo que la variable libre se vincula al valor del parámetro xde la función contenedora. 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, g, mientras 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 hubo, usado para definirlas es irrelevante.

Un cierre es un valor como cualquier otro. No necesita asignarse a una variable y 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 clausuras en sí mismas: tienen una variable libre que aún no está ligada. Solo cuando la función contenedora se evalúa con un valor para el parámetro, la variable libre de la función anidada se liga, creando una clausura, que luego es devuelta por la función contenedora.

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 diferencie (el enlace estático y el dinámico no se pueden distinguir porque los nombres se resuelven a 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 donde xse define, por lo que es irrelevante si se trata realmente de cierres:

x : int = 1 nums : lista [ int ] = [ 1 , 2 , 3 ]def f ( y : int ) -> int : return x + ymap ( f , nums ) map ( lambda y : x + y , nums )

Esto se consigue con mayor frecuencia mediante el retorno de una función, ya que la función debe definirse dentro del ámbito de las variables no locales, en cuyo caso su propio ámbito suele ser menor.

This can also be achieved by variable shadowing (which reduces the scope of the non-local variable), though this is less common in practice, as it is less useful and shadowing is discouraged. In this example f can be seen to be a closure because x in the body of f is bound to the x in the global namespace, not the x local to g:

x:int=0deff(y:int)->int:returnx+ydefg(z:int)->int:x:int=1# local x shadows global xreturnf(z)print(g(1))# prints 1, not 2

Applications

The use of closures is associated with languages where functions are first-class objects, in which functions can be returned as results from higher-order functions, or passed as arguments to other function calls; if functions with free variables are first-class, then returning one creates a closure. This includes functional programming languages such as Lisp and ML, and many modern, multi-paradigm languages, such as Julia, Python, and Rust. Closures are also often used with callbacks, particularly for event handlers, such as in JavaScript, where they are used for interactions with a dynamic web page.

Closures can also be used in a continuation-passing style to hide state. Constructs such as objects and control structures can thus be implemented with closures. In some languages, a closure may occur when a function is defined within another function, and the inner function refers to local variables of the outer function. At run-time, when the outer function executes, a closure is formed, consisting of the inner function's code and references (the upvalues) to any variables of the outer function required by the closure.

First-class functions

Los cierres suelen aparecer en lenguajes con funciones de primera clase ; en otras palabras, estos lenguajes permiten que las funciones se pasen como argumentos, se devuelvan desde llamadas a funciones, se asocien a nombres de variables, etc., al igual que tipos más simples como cadenas y enteros. Por ejemplo, considere la siguiente función de Scheme :

; Devuelve una lista de todos los libros con al menos THRESHOLD copias vendidas. ( define ( best-selling-books threshold ) ( filter ( lambda ( book ) ( >= ( book-sales book ) threshold )) book-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 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 a la filterfunción, que lo llama repetidamente para determinar qué libros se añadirán a la lista de resultados y cuáles se descartarán. Dado que el cierre tiene una referencia a threshold, puede usar esa variable cada vez filterque la llama. La función filterpodría estar definida en un archivo aparte.

Aquí está el mismo ejemplo reescrito en TypeScript , otro lenguaje popular que admite cierres:

// Devuelve una lista de todos los libros con al menos 'umbral' copias vendidas. function bestSellingBooks ( threshold : number ) : Book [] { return bookList . filter ( book => book . sales >= threshold ); }

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

tipo UnaryFunction = ( x : number ) => number ;// Devuelve una función que aproxima la derivada de f // usando un intervalo dx, que debe ser suficientemente pequeño. function derivative ( f : UnaryFunction , dx : number ) : UnaryFunction { 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 crea, las variables fy dxsiguen existiendo después de que la función derivativeretorne, aunque la ejecución haya salido de su ámbito y ya no sean visibles. En lenguajes sin cierres, la vida útil 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 comúnmente mediante algún tipo de recolección de basura .

Representación estatal

Un cierre se puede usar para asociar una función con un conjunto de variables " privadas ", que persisten tras varias invocaciones de la función. El ámbito de la variable abarca únicamente la función a la que está asociada, por lo que no se puede acceder a ella desde otro código del programa. Esto es análogo 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 functores) con un único método operador de llamada.

En lenguajes con estado, los cierres pueden utilizarse para implementar paradigmas de representación de estado y ocultamiento de información , ya que los valores superiores del cierre (sus variables ocultas) tienen una extensión indefinida , de modo 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 utilizan comúnmente en lenguajes funcionales impuros como Scheme .

Otros usos

Los cierres tienen muchos usos:

  • Dado que las clausuras retrasan la evaluación —es decir, no hacen nada hasta que se las llama—, pueden usarse para definir estructuras de control. Por ejemplo, todas las estructuras de control estándar de Smalltalk , incluidas las bifurcaciones (if/then/else) y los bucles (while y for), se definen mediante objetos cuyos métodos aceptan clausuras. 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 modificando dicho entorno. En Scheme:
( define foo #f ) ( define bar #f )( let (( secret-message "none" )) ( set! foo ( lambda ( msg ) ( set! secret-message msg ))) ( set! bar ( lambda () secret-message )))( display ( barra )) ; imprime "ninguno" ( salto de línea ) ( foo "nos vemos en los muelles a medianoche" ) ( display ( barra )) ; imprime "nos vemos en los muelles a medianoche"
  • Los cierres se pueden utilizar para implementar sistemas de objetos . [ 9 ]

Nota: Algunos hablantes denominan clausura a cualquier estructura de datos que vincula un entorno léxico , pero el término generalmente se refiere específicamente a funciones.

Implementación y teoría

Los cierres se implementan típicamente 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 de la creación del cierre, extendiendo además su tiempo de vida al menos hasta el mismo tiempo que el del cierre. Cuando se accede al cierre posteriormente, 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 admitir 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 dichos lenguajes, las variables locales automáticas de una función se liberan cuando la función finaliza. Sin embargo, un cierre requiere que las variables libres a las que hace referencia sobrevivan a la ejecución de la función que lo contiene. Por lo tanto, esas variables deben asignarse de manera que persistan hasta que ya no sean necesarias, generalmente mediante asignación en el montón , en lugar de en la pila, y su ciclo de vida debe gestionarse para que sobrevivan hasta que todos los cierres que las referencian dejen de utilizarse.

Esto explica por qué, normalmente, 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 variables no locales (asignación explícita en el montón y liberación cuando se hace), o, si se utiliza la asignación en la 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 las 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 D versión 1, 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 todavía permite muchos patrones funcionales útiles, pero para casos complejos necesita asignación explícita en el montón para las variables. La versión 2 de D solucionó este problema detectando qué variables deben almacenarse en el montón y realizando una asignación automática. Dado que D utiliza recolección de basura, en ambas versiones no es necesario realizar un seguimiento del uso de las 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 existen ciclos posibles en las referencias a 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 finaliza, 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 ámbito 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 puede copiar estos valores en la estructura de datos del cierre de una manera que es invisible para el programador.

Scheme , que cuenta con un sistema de ámbito léxico similar al de 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. En Scheme, las clausuras se expresan de forma natural. La forma lambda encierra el código, y las variables libres de su entorno persisten dentro del programa mientras se pueda acceder a ellas, por lo que pueden utilizarse con la misma libertad que cualquier otra expresión de Scheme.

Los cierres están estrechamente relacionados con los actores en el modelo de actores de computación concurrente, donde los valores en el entorno léxico de la función se denominan conocidos . Un aspecto importante para los cierres en los lenguajes de programación concurrente es si las variables dentro de un cierre pueden actualizarse y, de ser así, cómo se pueden sincronizar dichas 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 .

Diferencias en la semántica

Entorno léxico

Como los distintos lenguajes no siempre comparten una definición común del entorno léxico, sus definiciones de clausura también pueden variar. La definición minimalista más aceptada del entorno léxico lo define como el conjunto de todas las vinculaciones de variables en el ámbito, y eso es precisamente lo que las clausuras en cualquier lenguaje deben capturar. Sin embargo, el significado de una vinculación de variable también difiere. En los lenguajes imperativos, las variables se vinculan a ubicaciones relativas en la memoria que pueden almacenar valores. Si bien la ubicación relativa de una vinculación no cambia en tiempo de ejecución, el valor en la ubicación vinculada sí puede. En estos lenguajes, dado que la clausura captura la vinculación, cualquier operación sobre la variable, ya sea realizada desde la clausura o no, se realiza sobre la misma ubicación relativa de memoria. Esto se suele denominar captura de la variable "por referencia". A continuación, se muestra un ejemplo que ilustra el concepto en ECMAScript , uno de estos lenguajes:

let f : () => number ; let g : () => number ; function foo () { let x : number ; f = () => ++ x ; g = () => -- x ; x = 1 ; console . log ( `dentro de foo, llamada a f(): ${ f () } ` ); } foo (); // 2 console . log ( `llamada a g(): ${ g () } ` ); // 1 (--x) console . log ( `llamada a g(): ${ g () } ` ); // 0 (--x) console . log ( `llamada a f(): ${ f () } ` ); // 1 (++x) console . log ( `llamada a f(): ${ f () } ` ); // 2 (++x)

La función fooy los cierres a los que hacen referencia las variables fy gutilizan 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 no vinculada

[ 13 ]

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

Ejemplo 2: Referencia accidental a una variable ligada

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 ámbito anterior y se evalúa de forma diferida al hacer clic, lo que realmente sucede es que cada evento de clic emite el ID del último elemento en 'elements' 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 ( ) { console.log ( e.id ) ; } }

Nuevamente, aquí la variable etendría que estar limitada por el ámbito del bloque usando handle.bind(this)la letpalabra clave.

Por otro lado, muchos lenguajes funcionales, como ML , vinculan las variables directamente a valores. En este caso, dado que no hay forma de cambiar el valor de la variable una vez vinculada, no es necesario compartir el estado entre cierres; simplemente usan los mismos valores. Esto se suele denominar captura de 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 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 `<variable>` [&], que significa captura por referencia, o con `<variable> [=]`, que significa captura por valor.

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

-- Haskell foo :: Fractional 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 vinculación rcapturada por el cierre definido dentro de la función foose refiere al cálculo (x / y), lo 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 se intenta utilizar la vinculación capturada.

Cierre dejando

Sin embargo, se manifiestan más diferencias en el comportamiento de otras construcciones con ámbito léxico, como returnlas sentencias breaky continue. Dichas construcciones pueden, en general, considerarse en términos de invocar una continuación de escape establecida por una sentencia de control que las contiene (en el caso de breaky continue, dicha interpretación requiere que las construcciones de bucle se consideren en términos de llamadas a funciones 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 sentencia; por lo tanto, un 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 puede invocarse en Smalltalk implícitamente al llegar al final del código del cierre. Estos ejemplos en ECMAScript y Smalltalk resaltan la diferencia:

"Charla informal" 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 () : number { let xs : number [] = [ 1 , 2 , 3 , 4 ]; xs . forEach ( function ( x ) { return x ; }); return 0 ; } console . log ( foo ()); // imprime 0

Los fragmentos de código anteriores se comportarán de manera diferente porque el ^operador de Smalltalk y el returnoperador de JavaScript no son análogos. En el ejemplo de ECMAScript, return x;saldrá del 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 proporciona 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 JavaScriptreturn x . Por lo tanto, Smalltalk permite que una continuación de escape capturada sobreviva más allá del límite en el que puede ser invocada con éxito. Considere:

"Charla informal" 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 fooque creó dicho cierre. Dado que esa llamada ya ha finalizado y el modelo de invocación de métodos de Smalltalk no sigue la disciplina de pila de llamadas para facilitar múltiples retornos, esta operación produce un error.

Algunos lenguajes, como Ruby , permiten al programador elegir la forma en returnque se capturan los datos. Un ejemplo en Ruby:

# Rubí# Cierre usando un Proc def foo f = Proc . new { return "retorno de foo desde dentro del proc" } f . call # el control deja foo aquí return "retorno de foo" end# Cierre usando una lambda def bar f = lambda { return "retorno de la lambda" } f . call # el control no sale de bar aquí return "retorno de bar" endputs foo # imprime "retorno de foo desde dentro del proc" puts bar # imprime "retorno 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 returninstrucción de control son explícitos (y se la denomina arbitrariamente 'return' solo para este ejemplo). A continuación, se presenta 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 "retorno de foo desde dentro del proc" )) ( f ) ; el control deja foo aquí ( return "retorno de foo" ))))( define ( bar ) ( call/cc ( lambda ( return ) ( define ( f ) ( call/cc ( lambda ( return ) ( return "return from lambda" )))) ( f ) ; el control no sale de bar aquí ( return "return from bar" ))))( display ( foo )) ; imprime "retorno de foo desde dentro del proc" ( nueva línea ) ( display ( bar )) ; imprime "retorno de bar"

Estructuras similares a cierres

Algunos lenguajes poseen 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 llamada (C)

Algunas bibliotecas de C admiten funciones de devolución de llamada (callbacks) . Esto a veces se implementa proporcionando dos valores al registrar la función de devolución de llamada en la biblioteca: un puntero a la función y un void*puntero independiente a datos arbitrarios elegidos por el usuario. Cuando la biblioteca ejecuta la función de devolución de llamada, pasa el puntero a los datos. Esto permite que la función de devolución de llamada mantenga su estado y haga referencia a la información capturada al registrarse en la biblioteca. Este patrón es similar a los cierres en funcionalidad, pero no en sintaxis. El void*puntero no es de tipo seguro , por lo que este patrón de C difiere de los cierres de tipo seguro en C#, Haskell o ML.

Las funciones de devolución de llamada se utilizan ampliamente en los conjuntos de herramientas de widgets de interfaz gráfica de usuario (GUI) para implementar la programación orientada a eventos , asociando funciones generales de los widgets gráficos (menús, botones, casillas de verificación, controles deslizantes, selectores numéricos, etc.) con funciones específicas de la aplicación que implementan el comportamiento deseado para la misma.

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

Con una extensión de GNU Compiler Collection (GCC), se puede usar una función anidada [ 15 ]adder y un puntero a función puede emular cierres, siempre que la función no salga del ámbito que la contiene. El siguiente ejemplo no es válido porque 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 <stdio.h>typedef int ( * FnIntToInt )( int ); // tipo de función int->intFnIntToInt adder ( int number ) { int add ( int value ) { return value + number ; } 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 ) { FnIntToInt add10 = adder ( 10 ); printf ( "%d \ n " , add10 ( 1 )); return 0 ; }

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

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

Si se ejecuta, ahora imprime 11como se esperaba.

Clases locales y funciones lambda (Java)

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

import java.net.URI ; import javax.swing.JFrame ;class CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // La expresión "new Runnable() { ... }" es una clase anónima que implementa la interfaz 'Runnable'. Thread t = new Thread ( new Runnable () { @Override void run () { // Puede leer variables locales finales: calculate ( uri ); // Puede acceder a campos privados de la clase contenedora: result += 10 ; } } ); t . start (); } }

La captura de finalvariables permite capturar variables por valor. Incluso si la variable a capturar no es nula 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, un array de un solo elemento. La clase local no podrá modificar el valor de la referencia al contenedor, pero sí podrá modificar su contenido.

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

import java.net.URI ; import javax.swing.JFrame ;class CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // El código () -> { /* código */ } es un cierre. Thread t = new Thread (() -> { calculate ( uri ); result += 10 ; }); t . start (); } }

Las clases locales son un tipo de clase interna que se declara dentro del cuerpo de un método. Java también admite clases internas que se declaran como miembros no estáticos de una clase contenedora. [ 17 ] Normalmente se las denomina simplemente "clases internas". [ 18 ] Estas se definen en el cuerpo de la clase contenedora y tienen acceso completo a las variables de instancia de dicha clase. Debido a su vinculación con estas variables de instancia, una clase interna solo puede instanciarse con una vinculación explícita a una instancia de la clase contenedora mediante una sintaxis especial. [ 19 ]

public class EnclosingClass { // Define la clase interna: public class InnerClass { public int incrementAndReturnCounter () { return counter ++ ; } }contador entero privado ; { contador = 0 ; }public int getCounter () { return contador ; }public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); // Instanciar la clase interna, con enlace a la instancia EnclosingClass . InnerClass innerClassInstance = enclosingClassInstance . new InnerClass ();for ( int i = enclosingClassInstance . getCounter (); ( i = innerClassInstance . incrementAndReturnCounter ()) < 10 ; // paso de incremento omitido ) { System . out . println ( i ); } } }

Al ejecutarse, 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 el modificador "static"; estas últimas no tienen el efecto deseado, sino que son simplemente clases sin ninguna vinculación especial definida en una clase contenedora.

A partir de Java 8 , Java admite funciones como objetos de primera clase. Las expresiones lambda de esta forma se consideran de tipo , java.util.function.Function<T, U>donde T es el dominio y U el tipo de imagen. La expresión se puede llamar con su Function.apply(T t)método, pero no con una llamada a método estándar.

import java.util.function.Function ;Función < Cadena , Entero > longitud = s -> s . longitud (); Sistema . salida . imprimir ( longitud . aplicar ( "Hola, mundo!" )); // imprime '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 bloque se marcan 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 capturan por referencia se marcan con __block. Los bloques que necesitan persistir fuera del ámbito en el que se crean pueden necesitar ser copiados. [ 20 ] [ 21 ]

typedef int ( ^ IntBlock )();IntBlock downCounter ( int start ) { __block int i = start ; return [[ ^ int () { return i -- ; } copy ] autorelease ]; }IntBlock f = downCounter ( 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 cierres:

usando System.Collections.Generic ;int [] datos = new [] { 1 , 2 , 3 , 4 } ; int multiplicador = 2 ; List <int> resultado = datos.Select ( 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 datos = { 1 , 2 , 3 , 4 } Dim multiplicador = 2 Dim resultado = datos . Seleccionar ( Función ( x ) x * multiplicador )

En D , los cierres se implementan mediante delegados, un puntero a 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 el delegado }void bar () { auto dg = test1 (); dg (); // 10 (OK: test1.a está en un cierre y aún existe)dg = test2 (); dg (); // 25 (OK: test2.a está en un cierre y aún existe) }

La versión 1 de D tiene soporte limitado para cierres. 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 llamar a `foo` a través de `dg()` devuelva un entero aleatorio). Esto se puede solucionar 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 se usen solo mientras los valores referenciados 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 genérico de procesamiento de datos , por lo que esta limitación, en la práctica, no suele ser un problema.

Esta limitación se corrigió 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 salir del ámbito actual (mediante asignación a dg o retorno). Cualquier otra variable local (o argumento) que no sea referenciada por delegados o que solo sea referenciada por delegados que no salgan del ámbito actual, permanece en la pila, lo cual es más simple y rápido que la asignación en el montón. Lo mismo ocurre con los métodos de clase de la función interna que referencian 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 de forma similar a las funciones en un lenguaje de programación funcional. Pueden crearse en tiempo de ejecución y 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 especial del lenguaje 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 este último caso, si el objeto de cierre escapa del ámbito de un objeto referenciado, su invocación operator()provoca un comportamiento indefinido, ya que los cierres de C++ no extienden la vida útil de su contexto.

importar std ;usando std :: string ; usando std :: vector ;void foo ( string & name ) { int n = 100 ; vector < string > v ; // ...// i es de tipo vector<string>::iterator auto i = std :: ranges :: find_if ( v , [ & ]( const string & s ) -> bool { return s != name && s . size () > n ; }); // 'i' ahora es 'v.end()' o apunta a la primera cadena en 'v' // que no es igual a 'name' y cuya longitud es mayor que 'n' }

C++ también tiene el objeto std::function<R(Args...)>para almacenar una función anónima. [ 22 ] Sin embargo, debido a que las funciones anónimas tienen nombres generados por el compilador y son distintas, almacenar una como std::functionutiliza borrado de tipo, lo que causa una sobrecarga menor.

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 al proporcionar el código de la rutina en línea. Por ejemplo, en

ok_button . click_event . subscribe ( agent ( x , y : INTEGER ) do map . country_at_coordinates ( x , y ). display end )

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_eventde un botón determinado, de modo que cada vez que se produce una instancia de ese tipo de evento en ese botón (porque un usuario ha hecho clic en él), el procedimiento se ejecutará pasando las coordenadas del ratón 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 del ámbito que los contiene. Esta decisión de diseño ayuda a evitar ambigüedades al hablar del valor de una variable local en un cierre: ¿debe ser el último valor de la variable o el valor capturado cuando se crea el agente? Solo se puede acceder Currental objeto actual (una referencia al objeto actual, análoga a thisen Java), sus características y los 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__closure para proporcionar un puntero a un método con una sintaxis similar a la de un puntero a función. [ 23 ]

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

typedef void ( * TMyFunctionPointer )( void );

De manera similar, typedefse puede declarar 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 a función .
  2. Estos nombres suelen referirse 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) . Simposio Internacional sobre Tendencias en Programación Funcional . Lecture Notes in Computer Science. Vol.  7829. Springer. pp.  1–20. Véase 12 §2, nota 8 para la afirmación sobre las expresiones M. 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 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). Así, 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 entre 1966 y 1967. Fue entonces cuando me di cuenta de la correspondencia entre las listas FUNARG, que son el resultado de la evaluación de expresiones Lambda "cerradas" en LISP , y los cierres Lambda 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 las vinculaciones de sus variables libres, se cierra.
  6. Sussman, Gerald Jay ; Steele, Guy L. Jr. (diciembre de 1975). Scheme: Un intérprete para el cálculo lambda extendido (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. ¿Hay alguno que supere a los demás?" . 29 de diciembre de 1999. Archivado del original el 26 de diciembre de 2008. Recuperado el 23 de diciembre de 2008 .
  10. Expresiones Lambda y Cierres. Comité de Estándares de C++. 29 de febrero de 2008.
  11. "6.4 Funciones anidadas" . Manual de GCC . Si intentas llamar a la función anidada a través de su dirección después de que la función contenedora finalice, se producirán errores graves. Si intentas llamarla después de que finalice un nivel de ámbito contenedor, y si hace referencia a algunas de las variables que ya no están en el ámbito, podrías 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, no deberías tener problemas.
  12. Fundamentos de la semántica de actores Will Clinger. Tesis doctoral en matemáticas del MIT. Junio ​​de 1981.
  13. "Function.prototype.bind()" . MDN Web Docs . 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" . Los tutoriales de Java .
  17. "Clases anidadas, internas, de miembros y de nivel superior" . Blog de Joseph D. Darcy sobre Oracle . Julio de 2007. Archivado del original el 31 de agosto de 2016.
  18. "Ejemplo de clase interna" . Los tutoriales de Java: Aprendiendo el lenguaje Java: Clases y objetos .
  19. "Clases anidadas" . Los 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 del original el 25 de octubre de 2010. Recuperado el 18 de septiembre de 2010 .
  22. "std::function - cppreference.com" . cppreference.com . Consultado el 2 de septiembre de 2025 .
  23. 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 de tipado dinámico , por Martin Fowler .
  • Métodos de cierre de colecciones : Un ejemplo de un dominio técnico donde el uso de cierres resulta conveniente, por Martin Fowler.