functional-programming computer-science terminology glossary

functional programming - ¿Qué es un ''cierre''?



functional-programming computer-science (15)

Alcance variable

Cuando declara una variable local, esa variable tiene un alcance. Generalmente, las variables locales existen solo dentro del bloque o función en el que las declara.

function() { var a = 1; console.log(a); // works } console.log(a); // fails

Si trato de acceder a una variable local, la mayoría de los idiomas la buscarán en el ámbito actual, luego a través de los ámbitos primarios hasta que alcancen el ámbito raíz.

var a = 1; function() { console.log(a); // works } console.log(a); // works

Cuando se realiza un bloque o una función, sus variables locales ya no son necesarias y, por lo general, se pierden de memoria.

Así es como normalmente esperamos que las cosas funcionen.

Un cierre es un ámbito variable local persistente.

Un cierre es un alcance persistente que se mantiene en las variables locales incluso después de que la ejecución del código se haya movido fuera de ese bloque. Los idiomas que admiten el cierre (como JavaScript, Swift y Ruby) le permitirán mantener una referencia a un ámbito (incluidos sus ámbitos primarios), incluso después de que el bloque en el que se declararon esas variables haya terminado de ejecutarse, siempre que mantenga una referencia a Ese bloque o función funciona en algún lugar.

El objeto de alcance, y todas sus variables locales, están vinculados a la función, y persistirán mientras esa función persista.

Esto nos da la portabilidad de la función. Podemos esperar que cualquier variable que estuviera en el alcance cuando la función se definió por primera vez aún estuviera en el alcance cuando más tarde llamemos a la función, incluso si llamamos a la función en un contexto completamente diferente.

Por ejemplo

Aquí hay un ejemplo muy simple en JavaScript que ilustra el punto:

outer = function() { var a = 1; var inner = function() { console.log(a); } return inner; // this returns a function } var fnc = outer(); // execute outer to get inner fnc();

Aquí he definido una función dentro de una función. La función interna obtiene acceso a todas las variables locales de la función externa, incluyendo a . La variable a está en el alcance de la función interna.

Normalmente, cuando una función sale, todas sus variables locales son eliminadas. Sin embargo, si devolvemos la función interna y la asignamos a una variable fnc , para que persista después de que haya salido la outer también persisten todas las variables que estaban en el alcance cuando se definió la inner . La variable a ha sido cerrada sobre - está dentro de un cierre.

Tenga en cuenta que la variable a es totalmente privada a fnc . Esta es una forma de crear variables privadas en un lenguaje de programación funcional como JavaScript.

Como podría adivinar, cuando llamo fnc() imprime el valor de a , que es "1".

En un lenguaje sin cierre, la variable a habría sido recolectada y desechada cuando la función salió. Llamar a fnc habría generado un error porque ya no existe.

En JavaScript, la variable a persiste porque el alcance de la variable se crea cuando se declara la función por primera vez, y persiste mientras la función continúe existiendo.

a pertenece al ámbito de lo outer . El alcance de inner tiene un puntero principal al alcance de outer . fnc es una variable que apunta al inner . Persiste mientras fnc persista. a está dentro del cierre.

Hice una pregunta sobre el curry y se mencionaron los cierres. ¿Qué es un cierre? ¿Cómo se relaciona con el curry?



tl; dr

Un cierre es una función y su alcance se asigna a (o se usa como) una variable. Por lo tanto, el cierre de nombre: el alcance y la función se incluyen y utilizan como cualquier otra entidad.

Explicación detallada del estilo de Wikipedia.

Según Wikipedia, un cierre es:

Técnicas para implementar el enlace de nombres de ámbito léxico en lenguajes con funciones de primera clase.

Qué significa eso? Veamos algunas definiciones.

Explicaré los cierres y otras definiciones relacionadas usando este ejemplo:

function startAt(x) { return function (y) { return x + y; } } var closure1 = startAt(1); var closure2 = startAt(5); console.log(closure1(3)); // 4 (x == 1, y == 3) console.log(closure2(3)); // 8 (x == 5, y == 3)

Funciones de primera clase

Básicamente eso significa que podemos usar funciones como cualquier otra entidad . Podemos modificarlos, pasarlos como argumentos, devolverlos desde funciones o asignarlos para variables. Técnicamente hablando, son ciudadanos de primera clase , de ahí el nombre: funciones de primera clase.

En el ejemplo anterior, startAt devuelve una función ( anonymous ) cuya función se asigna al closure1 y al closure2 . Entonces, como ve, JavaScript trata las funciones como cualquier otra entidad (ciudadanos de primera clase).

Enlace de nombre

La vinculación de nombres consiste en averiguar a qué datos se refiere una variable (identificador). El alcance es realmente importante aquí, ya que eso es lo que determinará cómo se resuelve un enlace.

En el ejemplo anterior:

  • En el alcance de la función anónima interna, y está vinculado a 3 .
  • En el alcance de startAt , x está vinculado a 1 o 5 (dependiendo del cierre).

Dentro del alcance de la función anónima, x no está vinculado a ningún valor, por lo que debe resolverse en un alcance superior (de startAt ).

Alcance léxico

Como dice Wikipedia , el alcance:

Es la región de un programa de computadora donde el enlace es válido: donde el nombre se puede usar para referirse a la entidad .

Hay dos técnicas:

  • Alcance léxico (estático): la definición de una variable se resuelve buscando su bloque o función contenedora, luego, si eso falla al buscar el bloque contenedor externo, y así sucesivamente.
  • Alcance dinámico: se busca la función de llamada, luego la función que llamó a esa función de llamada, y así sucesivamente, avanzando hacia la pila de llamadas.

Para obtener más información, consulte esta pregunta y eche un vistazo a Wikipedia .

En el ejemplo anterior, podemos ver que JavaScript tiene un ámbito léxico, porque cuando se resuelve x , el enlace se busca en el ámbito superior (de startAt ), basado en el código fuente (la función anónima que busca x está definida dentro de startAt ) y no se basa en la pila de llamadas, la forma (el ámbito donde) se llamó la función.

Envolviendo (cerrando)

En nuestro ejemplo, cuando llamamos startAt , devolverá una función (de primera clase) que se asignará a closure1 y closure2 lo que se crea un cierre, porque las variables 1 y 5 aprobadas se guardarán dentro del alcance de startAt , que se adjuntará con la función anónima devuelta. Cuando llamamos a esta función anónima mediante closure1 y closure2 con el mismo argumento ( 3 ), el valor de y se encontrará inmediatamente (ya que ese es el parámetro de esa función), pero x no está vinculado en el alcance de la función anónima, por lo tanto, la resolución continúa en el alcance de la función superior (lexicamente) (que se guardó en el cierre) donde se encuentra que x está vinculada a 1 o 5 . Ahora sabemos todo para el resumen, por lo que el resultado se puede devolver y luego imprimir.

Ahora debes entender los cierres y cómo se comportan, que es una parte fundamental de JavaScript.

Zurra

Ah, y también aprendió de qué se trata el currying : usa funciones (cierres) para pasar cada argumento de una operación en lugar de usar una función con varios parámetros.


Aquí hay otro ejemplo de la vida real, y usar un lenguaje de scripting popular en los juegos - Lua. Necesitaba cambiar ligeramente la forma en que funcionaba una función de biblioteca para evitar un problema con la falta de disponibilidad de stdin.

local old_dofile = dofile function dofile( filename ) if filename == nil then error( ''Can not use default of stdin.'' ) end old_dofile( filename ) end

El valor de old_dofile desaparece cuando este bloque de código finaliza su alcance (porque es local), sin embargo, el valor se incluyó en un cierre, por lo que la nueva función redefinida de dofile PUEDE acceder a ella, o más bien una copia almacenada junto con la función como un ''upvalue''.


Aquí hay un ejemplo del mundo real de por qué los cierres patean el culo ... Esto es directamente de mi código Javascript. Déjame ilustrar.

Function.prototype.delay = function(ms /*[, arg...]*/) { var fn = this, args = Array.prototype.slice.call(arguments, 1); return window.setTimeout(function() { return fn.apply(fn, args); }, ms); };

Y así es como lo usarías:

var startPlayback = function(track) { Player.play(track); }; startPlayback(someTrack);

Ahora imagine que desea que la reproducción comience con retraso, como, por ejemplo, 5 segundos después de que se ejecute este fragmento de código. Bueno, eso es fácil con delay y es el cierre:

startPlayback.delay(5000, someTrack); // Keep going, do other things

Cuando se llama a la delay con 5000 ms, el primer fragmento de código se ejecuta y almacena los argumentos pasados ​​en su cierre. Luego, 5 segundos después, cuando se setTimeout devolución de llamada setTimeout , el cierre aún mantiene esas variables, por lo que puede llamar a la función original con los parámetros originales.
Este es un tipo de curry, o función de decoración.

Sin los cierres, tendrías que mantener de alguna manera el estado de esas variables fuera de la función, por lo tanto ensuciar el código fuera de la función con algo que lógicamente pertenece a ella. El uso de cierres puede mejorar en gran medida la calidad y la legibilidad de su código.


Cierres Siempre que tenemos una función definida dentro de otra función, la función interna tiene acceso a las variables declaradas en la función externa. Los cierres se explican mejor con ejemplos. En el Listado 2-18, puede ver que la función interna tiene acceso a una variable (variableInOuterFunction) desde el ámbito externo. Las variables en la función externa han sido cerradas por (o ligadas a) la función interna. De ahí el término cierre. El concepto en sí mismo es bastante simple y bastante intuitivo.

Listing 2-18: function outerFunction(arg) { var variableInOuterFunction = arg; function bar() { console.log(variableInOuterFunction); // Access a variable from the outer scope } // Call the local function to demonstrate that it has access to arg bar(); } outerFunction(''hello closure!''); // logs hello closure!

fuente: http://index-of.es/Varios/Basarat%20Ali%20Syed%20(auth.)-Beginning%20Node.js-Apress%20(2014).pdf


Daré un ejemplo (en JavaScript):

function makeCounter () { var count = 0; return function () { count += 1; return count; } } var x = makeCounter(); x(); returns 1 x(); returns 2 ...etc...

Lo que hace esta función, makeCounter, es que devuelve una función, que hemos llamado x, que contará en una cada vez que se llame. Ya que no estamos proporcionando ningún parámetro a x, de alguna manera debemos recordar el conteo. Sabe dónde encontrarlo en función de lo que se denomina alcance léxico: debe buscar el lugar donde está definido para encontrar el valor. Este valor "oculto" es lo que se llama un cierre.

Aquí está mi ejemplo de curry otra vez:

function add (a) { return function (b) { return a + b; } } var add3 = add(3); add3(4); returns 7

Lo que puede ver es que cuando llama a add con el parámetro a (que es 3), ese valor está contenido en el cierre de la función devuelta que estamos definiendo como add3. De esa manera, cuando llamamos a add3, sabe dónde encontrar el valor para realizar la suma.


De Lua.org :

Cuando una función se escribe encerrada en otra función, tiene acceso completo a las variables locales desde la función encerradora; esta característica se llama alcance léxico. Aunque eso pueda parecer obvio, no lo es. El alcance léxico, más las funciones de primera clase, es un concepto poderoso en un lenguaje de programación, pero pocos lenguajes soportan ese concepto.


En primer lugar, al contrario de lo que la mayoría de las personas aquí le dicen, ¡el cierre no es una función ! Entonces, ¿qué es ?
Es un conjunto de símbolos definidos en el "contexto circundante" de una función (conocido como su entorno ) que lo convierten en una expresión CERRADA (es decir, una expresión en la que cada símbolo está definido y tiene un valor, por lo que puede evaluarse).

Por ejemplo, cuando tienes una función de JavaScript:

function closed(x) { return x + 3; }

es una expresión cerrada porque todos los símbolos que aparecen en ella están definidos en ella (sus significados son claros), por lo que puede evaluarla. En otras palabras, es autocontenido .

Pero si tienes una función como esta:

function open(x) { return x*y + 3; }

es una expresión abierta porque hay símbolos en ella que no se han definido en ella. A saber, y . Al observar esta función, no podemos decir qué es y qué significa, no sabemos su valor, por lo que no podemos evaluar esta expresión. Es decir, no podemos llamar a esta función hasta que indiquemos lo que se supone que significa en ella. Esta y se llama variable libre .

Esta definición requiere una definición, pero esta definición no es parte de la función, se define en otro lugar, en su "contexto circundante" (también conocido como el entorno ). Al menos eso es lo que esperamos: P

Por ejemplo, podría definirse globalmente:

var y = 7; function open(x) { return x*y + 3; }

O podría definirse en una función que lo envuelve:

var global = 2; function wrapper(y) { var w = "unused"; return function(x) { return x*y + 3; } }

La parte del entorno que da significado a las variables libres en una expresión, es el cierre . Se llama así, porque convierte una expresión abierta en una cerrada , al proporcionar estas definiciones faltantes para todas sus variables libres , para que podamos evaluarlas.

En el ejemplo anterior, la función interna (que no le dimos un nombre porque no la necesitábamos) es una expresión abierta porque la variable y en ella es libre ; su definición está fuera de la función, en la función que envuelve eso. El entorno para esa función anónima es el conjunto de variables:

{ global: 2, w: "unused", y: [whatever has been passed to that wrapper function as its parameter `y`] }

Ahora, el cierre es la parte de este entorno que cierra la función interna proporcionando las definiciones de todas sus variables libres . En nuestro caso, la única variable libre en la función interna era y , por lo que el cierre de esa función es este subconjunto de su entorno:

{ y: [whatever has been passed to that wrapper function as its parameter `y`] }

Los otros dos símbolos definidos en el entorno no forman parte del cierre de esa función, ya que no requiere que se ejecuten. No son necesarios para cerrarla .

Más sobre la teoría detrás de esto aquí: https://.com/a/36878651/434562

Vale la pena notar que en el ejemplo anterior, la función de envoltura devuelve su función interna como un valor. El momento en que llamamos a esta función puede ser remoto en el tiempo desde el momento en que la función se ha definido (o creado). En particular, su función de envoltura ya no se está ejecutando, y sus parámetros que han estado en la pila de llamadas ya no están ahí: P Esto crea un problema, porque la función interna necesita y para estar allí cuando se llama. En otras palabras, requiere las variables desde su cierre para sobrevivir de alguna manera a la función de envoltura y estar allí cuando sea necesario. Por lo tanto, la función interna tiene que hacer una instantánea de estas variables que hacen su cierre y almacenarlas en un lugar seguro para su uso posterior. (En algún lugar fuera de la pila de llamadas).

Y esta es la razón por la que las personas a menudo confunden el término cierre con ese tipo especial de función que puede hacer esas instantáneas de las variables externas que usan, o la estructura de datos utilizada para almacenar estas variables para más adelante. Pero espero que entienda ahora que no son el cierre en sí mismo, solo son formas de implementar cierres en un lenguaje de programación o mecanismos de lenguaje que permiten que las variables del cierre de la función estén ahí cuando sea necesario. Hay muchos conceptos erróneos en torno a los cierres que (innecesariamente) hacen que este tema sea mucho más confuso y complicado de lo que realmente es.


En resumen, el puntero de función es solo un puntero a una ubicación en la base del código del programa (como el contador del programa). Considerando que Closure = puntero de función + marco de pila .

.


En una situación normal, las variables están limitadas por la regla de alcance: las variables locales solo funcionan dentro de la función definida. El cierre es una forma de romper esta regla temporalmente por conveniencia.

def n_times(a_thing) return lambda{|n| a_thing * n} end

en el código anterior, lambda(|n| a_thing * n} es el cierre porque a la a_thing es referida por lambda (un creador de funciones anónimo).

Ahora, si pones la función anónima resultante en una variable de función.

foo = n_times(4)

foo romperá la regla de alcance normal y comenzará a usar 4 internamente.

foo.call(3)

devuelve 12.


Para ayudar a facilitar la comprensión de los cierres, podría ser útil examinar cómo podrían implementarse en un lenguaje de procedimiento. Esta explicación seguirá a una implementación simplista de cierres en el Esquema.

Para empezar, debo introducir el concepto de espacio de nombres. Cuando ingresa un comando en un intérprete de Esquema, debe evaluar los diversos símbolos en la expresión y obtener su valor. Ejemplo:

(define x 3) (define y 4) (+ x y) returns 7

Las expresiones definidas almacenan el valor 3 en el punto para x y el valor 4 en el punto para y. Luego, cuando llamamos (+ xy), el intérprete busca los valores en el espacio de nombres y puede realizar la operación y devolver 7.

Sin embargo, en el Esquema hay expresiones que le permiten anular temporalmente el valor de un símbolo. Aquí hay un ejemplo:

(define x 3) (define y 4) (let ((x 5)) (+ x y)) returns 9 x returns 3

Lo que hace la palabra clave let es introducir un nuevo espacio de nombres con x como el valor 5. Notará que todavía puede ver que y es 4, lo que hace que la suma devuelta sea 9. También puede ver que una vez que la expresión ha terminado x ha vuelto a ser 3. En este sentido, x ha sido enmascarado temporalmente por el valor local.

Los lenguajes procesales y orientados a objetos tienen un concepto similar. Cada vez que declara una variable en una función que tiene el mismo nombre que una variable global, obtiene el mismo efecto.

¿Cómo implementaríamos esto? Una forma simple es con una lista vinculada: la cabecera contiene el nuevo valor y la cola contiene el espacio de nombres anterior. Cuando necesitas buscar un símbolo, empiezas por la cabeza y avanzas por la cola.

Ahora vamos a pasar a la implementación de funciones de primera clase por el momento. Más o menos, una función es un conjunto de instrucciones que se ejecutan cuando se llama a la función que culmina con el valor de retorno. Cuando leemos una función, podemos almacenar estas instrucciones detrás de escena y ejecutarlas cuando se llama a la función.

(define x 3) (define (plus-x y) (+ x y)) (let ((x 5)) (plus-x 4)) returns ?

Definimos x para ser 3 y más-x para ser su parámetro, y, más el valor de x. Finalmente, llamamos a más-x en un entorno donde x ha sido enmascarado por un nuevo x, este valor 5. Si solo almacenamos la operación, (+ xy), para la función más-x, ya que estamos en el contexto de x siendo 5 el resultado devuelto sería 9. Esto es lo que se denomina alcance dinámico.

Sin embargo, Scheme, Common Lisp y muchos otros idiomas tienen lo que se denomina definición léxica: además de almacenar la operación (+ xy), también almacenamos el espacio de nombres en ese punto en particular. De esa manera, cuando buscamos los valores, podemos ver que x, en este contexto, es realmente 3. Esto es un cierre.

(define x 3) (define (plus-x y) (+ x y)) (let ((x 5)) (plus-x 4)) returns 7

En resumen, podemos usar una lista enlazada para almacenar el estado del espacio de nombres en el momento de la definición de la función, lo que nos permite acceder a las variables desde los ámbitos que lo rodean, así como la capacidad de enmascarar una variable localmente sin afectar el resto de los ámbitos. programa.


Si eres del mundo de Java, puedes comparar un cierre con una función miembro de una clase. Mira este ejemplo

var f=function(){ var a=7; var g=function(){ return a; } return g; }

La función g es un cierre: g cierra a in. Así que g puede compararse con una función miembro, a puede compararse con un campo de clase, y la función f con una clase.


Un cierre es una función que puede hacer referencia al estado en otra función. Por ejemplo, en Python, esto usa el cierre "interno":

def outer (a): b = "variable in outer()" def inner (c): print a, b, c return inner # Now the return value from outer() can be saved for later func = outer ("test") func (1) # prints "test variable in outer() 1


La respuesta de Kyle es bastante buena. Creo que la única aclaración adicional es que el cierre es básicamente una instantánea de la pila en el punto en que se crea la función lambda. Luego, cuando la función se vuelve a ejecutar, la pila se restaura a ese estado antes de ejecutar la función. Por lo tanto, como menciona Kyle, ese valor oculto ( count ) está disponible cuando se ejecuta la función lambda.