¿Por qué un programa usaría un cierre?

54

Después de leer muchas publicaciones que explican los cierres aquí, todavía me falta un concepto clave: ¿Por qué escribir un cierre? ¿Qué tarea específica realizaría un programador que podría ser mejor atendida por un cierre?

Ejemplos de cierres en Swift son los accesos de un NSUrl y el uso del geocodificador inverso. Este es uno de estos ejemplos. Desafortunadamente, esos cursos solo presentan el cierre. no explican por qué la solución de código está escrita como un cierre.

Un ejemplo de un problema de programación en mundo real que podría hacer que mi cerebro diga "aha, debería escribir un cierre para esto", sería más informativo que una discusión teórica. No hay escasez de discusiones teóricas disponibles en este sitio.

    
pregunta Bendrix 05.06.2015 - 15:36

10 respuestas

31

En primer lugar, no hay nada que sea imposible sin usar cierres. Siempre puede reemplazar un cierre por un objeto implementando una interfaz específica. Es solo una cuestión de brevedad y acoplamiento reducido.

Segundo, tenga en cuenta que los cierres a menudo se usan de manera inapropiada, donde una referencia de función simple u otra construcción sería más clara. No debe tomar todos los ejemplos que considere una mejor práctica.

Cuando los cierres realmente brillan sobre otras construcciones es cuando se usan funciones de orden superior, cuando realmente necesita para comunicar el estado, y puede convertirlo en una sola línea, como en este ejemplo de JavaScript de página de wikipedia sobre cierres :

// Return a list of all books with at least 'threshold' copies sold.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Aquí, threshold se comunica de manera muy sucinta y natural desde donde se define hasta donde se usa. Su alcance es precisamente lo más limitado posible. filter no tiene que escribirse para permitir la posibilidad de pasar datos definidos por el cliente como un umbral. No tenemos que definir ninguna estructura intermedia con el único propósito de comunicar el umbral en esta pequeña función. Es completamente autónomo.

Usted puede escribir esto sin un cierre, pero requerirá mucho más código y será más difícil de seguir. Además, JavaScript tiene una sintaxis lambda bastante verbosa. En Scala, por ejemplo, todo el cuerpo de la función sería:

bookList filter (_.sales >= threshold)

Si, sin embargo, puede utilizar ECMAScript 6 , gracias a funciones de flecha gruesa incluso el código JavaScript se vuelve mucho más simple y puede colocarse en una sola línea.

const bestSellingBooks = (threshold) => bookList.filter(book => book.sales >= threshold);

En su propio código, busque lugares donde genere una gran cantidad de repeticiones solo para comunicar valores temporales de un lugar a otro. Estas son excelentes oportunidades para considerar la sustitución con un cierre.

    
respondido por el Karl Bielefeldt 16.06.2015 - 19:41
49

A modo de explicación, voy a pedir prestado un código de esta excelente publicación de blog sobre cierres . Es JavaScript, pero ese es el idioma que utilizan la mayoría de las publicaciones de blog que hablan sobre cierres, porque los cierres son muy importantes en JavaScript.

Supongamos que desea generar una matriz como una tabla HTML. Podrías hacerlo así:

function renderArrayAsHtmlTable (array) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + object + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Pero estás a merced de JavaScript en cuanto a cómo se representará cada elemento de la matriz. Si desea controlar la representación, puede hacer esto:

function renderArrayAsHtmlTable (array, renderer) {
  var table = "<table>";
  for (var idx in array) {
    var object = array[idx];
    table += "<tr><td>" + renderer(object) + "</td></tr>";
  }
  table += "</table>";
  return table;
}

Y ahora solo puede pasar una función que devuelve la representación que desea.

¿Qué sucede si desea mostrar un total acumulado en cada fila de la tabla? Necesitarías una variable para rastrear ese total, ¿no? Un cierre le permite escribir una función de renderizador que se cierra sobre la variable total en ejecución, y le permite escribir un renderizador que puede hacer un seguimiento del total en ejecución:

function intTableWithTotals (intArray) {
  var total = 0;
  var renderInt = function (i) {
    total += i;
    return "Int: " + i + ", running total: " + total;
  };
  return renderObjectsInTable(intArray, renderInt);
}

La magia que está sucediendo aquí es que renderInt retiene el acceso a la variable total , incluso aunque renderInt se llame repetidamente y salga.

En un lenguaje más tradicionalmente orientado a objetos que JavaScript, podría escribir una clase que contenga esta variable total y pasarla en lugar de crear un cierre. Pero un cierre es una forma mucho más poderosa, limpia y elegante de hacerlo.

Lecturas adicionales

respondido por el Robert Harvey 05.06.2015 - 16:27
22

El propósito de closures es simplemente preservar estado; por lo tanto, el nombre closure - se cierra sobre el estado. Para facilitar la explicación, usaré Javascript.

Normalmente tienes una función

function sayHello(){
    var txt="Hello";
    return txt;
}

donde el alcance de las variables está vinculado a esta función. Entonces, después de la ejecución, la variable txt queda fuera del alcance. No hay forma de acceder o usarla después de que la función haya finalizado su ejecución.

Los cierres son construcciones de lenguaje, que permiten, como se dijo anteriormente, preservar el estado de las variables y prolongar el alcance.

Esto podría ser útil en diferentes casos. Un caso de uso es la construcción de funciones de orden superior .

  

En matemáticas y ciencias de la computación, una función de orden superior (también forma funcional, funcional o functor) es una función que realiza al menos uno de los siguientes: 1

     
  • toma una o más funciones como entrada
  •   
  • genera una función
  •   

Un ejemplo simple, pero no muy útil, es:

 makeadder=function(a){
     return function(b){
         return a+b;
     }
 }

 add5=makeadder(5);
 console.log(add5(10)); 

Usted define una función makedadder , que toma un parámetro como entrada y devuelve una función . Hay una función externa function(a){} y una interna function(b){}{} . Además, define (implícitamente) otra función add5 como resultado de llamar a la función de orden superior% código%. makeadder devuelve una función anónima ( inner ), que a su vez toma 1 parámetro y devuelve la suma del parámetro de la función outer y el parámetro de la Función interna .

El truco es que, mientras se devuelve la función inner , que realiza la adición real, se conserva el alcance del parámetro de la función externa ( makeadder(5) ) . a recuerda , que el parámetro add5 era a .

O para mostrar un ejemplo al menos de alguna manera útil:

  makeTag=function(openTag, closeTag){
     return function(content){
         return openTag +content +closeTag;
     }
 }

 table=makeTag("<table>","</table>")
 tr=makeTag("<tr>", "</tr>");
 td=makeTag("<td>","</td>");
 console.log(table(tr(td("I am a Row"))));

Otro caso de uso común es el denominado IIFE = expresión de función inmediatamente invocada. Es muy común en javascript para falsificar variables de miembros privados. Esto se realiza a través de una función, que crea un private scope = 5 , porque se encuentra inmediatamente después de la definición invocada. La estructura es closure . Observe los corchetes function(){}() después de la definición. Esto hace que sea posible utilizarlo para la creación de objetos con revelando el patrón del módulo . El truco es crear un alcance y devolver un objeto, que tenga acceso a este alcance después de la ejecución del IIFE.

El ejemplo de Addi se ve así:

 var myRevealingModule = (function () {

         var privateVar = "Ben Cherry",
             publicVar = "Hey there!";

         function privateFunction() {
             console.log( "Name:" + privateVar );
         }

         function publicSetName( strName ) {
             privateVar = strName;
         }

         function publicGetName() {
             privateFunction();
         }


         // Reveal public pointers to
         // private functions and properties

         return {
             setName: publicSetName,
             greeting: publicVar,
             getName: publicGetName
         };

     })();

 myRevealingModule.setName( "Paul Kinlan" );

El objeto devuelto tiene referencias a funciones (por ejemplo, () ), que a su vez tienen acceso a las variables "privadas" publicSetName .

Pero estos son casos de uso más especiales para Javascript.

  

¿Qué tarea específica estaría realizando un programador que podría ser mejor atendida por un cierre?

Hay varias razones para eso. Uno podría ser que es natural para él, ya que sigue un paradigma funcional . O en Javascript: es mera necesidad confiar en los cierres para sortear algunas peculiaridades del lenguaje.

    
respondido por el Thomas Junk 05.06.2015 - 19:20
16

Hay dos casos de uso principales para cierres:

  1. Asincronía. Supongamos que desea realizar una tarea que demorará un tiempo y luego hacer algo cuando haya terminado. Puede hacer que su código espere a que se haga, lo que bloquea la ejecución adicional y puede hacer que su programa no responda, o puede llamar a su tarea de forma asíncrona y decir "comience esta tarea larga en segundo plano, y cuando finalice, ejecute este cierre", donde el cierre contiene el código para ejecutar cuando se hace.

  2. Devolución de llamadas. También se conocen como "delegados" o "manejadores de eventos" según el idioma y la plataforma. La idea es que tenga un objeto personalizable que, en ciertos puntos bien definidos, ejecutará un evento , que ejecuta un cierre que pasa por el código que lo configura. Por ejemplo, en la interfaz de usuario de su programa, puede tener un botón y darle un cierre que contiene el código que se ejecutará cuando el usuario haga clic en el botón.

Hay otros usos para los cierres, pero esos son los dos principales.

    
respondido por el Mason Wheeler 05.06.2015 - 16:09
13

Un par de otros ejemplos:

Ordenar
La mayoría de las funciones de clasificación operan comparando pares de objetos. Se necesita alguna técnica de comparación. Restringir la comparación a un operador específico significa una clasificación bastante inflexible. Un enfoque mucho mejor es recibir una función de comparación como un argumento de la función de clasificación. A veces, una función de comparación sin estado funciona bien (por ejemplo, ordenar una lista de números o nombres), pero ¿qué pasa si la comparación necesita un estado?

Por ejemplo, considere ordenar una lista de ciudades por distancia a algún lugar específico. Una solución fea es almacenar las coordenadas de esa ubicación en una variable global. Esto hace que la función de comparación en sí misma sea sin estado, pero a costa de una variable global.

Este enfoque impide tener múltiples subprocesos clasificando simultáneamente la misma lista de ciudades por su distancia a dos ubicaciones diferentes. Un cierre que encierra la ubicación resuelve este problema y elimina la necesidad de una variable global.


Números aleatorios
El rand() original no tomó argumentos. Los generadores de números pseudoaleatorios necesitan estado. Algunos (por ejemplo, Mersenne Twister) necesitan muchos estados. Incluso el simple pero terrible rand() necesitaba estado. Lea un diario de matemáticas en un nuevo generador de números aleatorios e inevitablemente verá variables globales. Eso es bueno para los desarrolladores de la técnica, no tan bueno para los que llaman. Encapsular ese estado en una estructura y pasar la estructura al generador de números aleatorios es una forma de solucionar el problema de los datos globales. Este es el enfoque utilizado en muchos idiomas que no son OO para hacer que un generador de números aleatorios vuelva a ingresar. Un cierre oculta ese estado de la persona que llama. Un cierre ofrece la secuencia de llamada simple de rand() y la reentrada del estado encapsulado.

Hay más para números aleatorios que solo un PRNG. La mayoría de las personas que quieren aleatoriedad quieren que se distribuya de cierta manera. Comenzaré con números extraídos al azar de entre 0 y 1, o U (0,1) para abreviar. Cualquier PRNG que genere enteros entre 0 y algún máximo hará; simplemente divida (como punto flotante) el entero aleatorio por el máximo. Una forma conveniente y genérica de implementar esto es crear un cierre que requiera un cierre (el PRNG) y el máximo como entradas. Ahora tenemos un generador aleatorio genérico y fácil de usar para U (0,1).

Hay una serie de otras distribuciones además de U (0,1). Por ejemplo, una distribución normal con una cierta media y desviación estándar. Cada algoritmo generador de distribución normal que he encontrado utiliza un generador U (0,1). Una forma conveniente y genérica de crear un generador normal es crear un cierre que encapsule el generador de U (0,1), la media y la desviación estándar como estado. Esto es, al menos conceptualmente, un cierre que toma un cierre que toma un cierre como un argumento.

    
respondido por el David Hammen 05.06.2015 - 22:37
7

Los cierres son equivalentes a los objetos que implementan un método run (), e inversamente, los objetos se pueden emular con cierres.

  • La ventaja de los cierres es que se pueden usar fácilmente en cualquier lugar donde se espera una función: a.k.a. funciones de orden superior, devoluciones de llamada simples (o patrón de estrategia). No es necesario definir una interfaz / clase para crear cierres ad-hoc.

  • La ventaja de los objetos es la posibilidad de tener interacciones más complejas: múltiples métodos y / o diferentes interfaces.

Por lo tanto, usar el cierre u objetos es principalmente una cuestión de estilo. Este es un ejemplo de las cosas que los cierres facilitan pero que no es conveniente implementar con objetos:

 (let ((seen))
    (defun register-name (name)
       (pushnew name seen :test #'string=))

    (defun all-names ()
       (copy-seq seen))

    (defun reset-name-registry ()
       (setf seen nil)))

Básicamente, encapsula un estado oculto al que se accede solo a través de cierres globales: no necesita referirse a ningún objeto, solo use el protocolo definido por las tres funciones.

Confío en el primer comentario de supercat sobre el hecho de que en algunos idiomas es posible controlar con precisión la vida útil de los objetos, mientras que lo mismo no es cierto para los cierres. Sin embargo, en el caso de los lenguajes recolectados en la basura, la vida útil de los objetos generalmente es ilimitada, y por lo tanto es posible construir un cierre que podría llamarse en un contexto dinámico donde no debería llamarse (lectura de un cierre después de un flujo). está cerrado, por ejemplo).

Sin embargo, es bastante sencillo evitar este uso incorrecto capturando una variable de control que protegerá la ejecución de un cierre. Más precisamente, esto es lo que tengo en mente (en Common Lisp):

(defun guarded (function)
  (let ((active t))
    (values (lambda (&rest args)
              (when active
                (apply function args)))
            (lambda ()
              (setf active nil)))))

Aquí, tomamos un designador de función function y devolvemos dos cierres, ambos capturando una variable local llamada active :

  • el primero delega a function , solo cuando active es verdadero
  • el segundo establece action a nil , a.k.a. false .

En lugar de (when active ...) , por supuesto, es posible tener una expresión (assert active) , que podría generar una excepción en caso de que se llame al cierre cuando no debería ser. Además, tenga en cuenta que el código inseguro ya puede generar una excepción por sí solo cuando se usa de manera incorrecta, por lo que rara vez se necesita una envoltura de este tipo.

Aquí es cómo lo usarías:

(use-package :metabang-bind) ;; for bind

(defun example (obj1 obj2)
  (bind (((:values f f-deactivator)(guarded (lambda () (do-stuff obj1))))
         ((:values g g-deactivator)(guarded (lambda () (do-thing obj2)))))

    ;; ensure the closure are inactive when we exit
    (unwind-protect
         ;; pass closures to other functions
         (progn
           (do-work f)
           (do-work g))

      ;; cleanup code: deactivate closures
      (funcall f-deactivator)
      (funcall g-deactivator))))

Tenga en cuenta que los cierres de desactivación también podrían darse a otras funciones; aquí, las variables locales active no se comparten entre f y g ; también, además de active , f solo se refiere a obj1 y g solo se refiere a obj2 .

El otro punto mencionado por supercat es que los cierres pueden provocar fugas de memoria, pero desafortunadamente, es el caso de casi todo en entornos de recolección de basura. Si están disponibles, esto se puede resolver con punteros débiles (el cierre en sí podría mantenerse en la memoria, pero no evita la recolección de basura de otros recursos).

    
respondido por el coredump 05.06.2015 - 19:30
6

Nada que no se haya dicho ya, pero tal vez un ejemplo más simple.

Aquí hay un ejemplo de JavaScript que usa tiempos de espera:

// Example function that logs something to the browser's console after a given delay
function delayedLog(message, delay) {
  // this function will be called when the timer runs out
  var fire = function () {
    console.log(message); // closure magic!
  };

  // set a timeout that'll call fire() after a delay
  setTimeout(fire, delay);
}

Lo que sucede aquí es que cuando se llama a delayedLog() , regresa inmediatamente después de configurar el tiempo de espera, y el tiempo de espera sigue marcando en segundo plano.

Pero cuando el tiempo de espera se agota y llama a la función fire() , la consola mostrará el message que se pasó originalmente a delayedLog() , porque todavía está disponible para fire() a través del cierre. Puede llamar a delayedLog() todo lo que desee, con un mensaje y demora diferente cada vez, y hará lo correcto.

Pero imaginemos que JavaScript no tiene cierres.

Una forma sería hacer el bloqueo de setTimeout() , más como una función de "suspensión", por lo que el alcance de delayedLog() no desaparece hasta que se agote el tiempo de espera. Pero bloquear todo no es muy bueno.

Otra forma sería colocar la variable message en algún otro ámbito al que se pueda acceder después de que se haya ido el ámbito de delayedLog() .

Puede usar variables globales, o al menos de "ámbito amplio", pero tendrá que averiguar cómo realizar un seguimiento de qué mensaje va con qué tiempo de espera. Pero no puede ser simplemente una cola FIFO secuencial, porque puede establecer cualquier retraso que desee. Así que podría ser "primero en entrar, tercero en salir" o algo así. Por lo tanto, necesitaría otros medios para vincular una función temporizada a las variables que necesita.

Podría crear una instancia de un objeto de tiempo de espera que "agrupa" el temporizador con el mensaje. El contexto de un objeto es más o menos un ámbito que se mantiene. Luego tendrías que ejecutar el temporizador en el contexto del objeto, para que tuviera acceso al mensaje correcto. Pero tendrías que almacenar ese objeto porque sin ninguna referencia se recolectaría la basura (sin cierres, tampoco habría referencias implícitas a él). Y tendrías que eliminar el objeto una vez que se dispara su tiempo de espera, de lo contrario solo se quedará. Así que necesitaría algún tipo de lista de objetos de tiempo de espera, y revisarla periódicamente en busca de objetos "gastados" para eliminar, o los objetos se agregarían y eliminarían de la lista, y ...

Entonces ... sí, esto se está volviendo aburrido.

Afortunadamente, no tiene que usar un alcance más amplio, o disputar objetos solo para mantener ciertas variables alrededor. Debido a que JavaScript tiene cierres, ya tiene exactamente el alcance que necesita. Un ámbito que le da acceso a la variable message cuando la necesita. Y debido a eso, puede salirse con la suya escribiendo delayedLog() como arriba.

    
respondido por el Flambino 06.06.2015 - 13:25
3

PHP se puede usar para mostrar un ejemplo real en un idioma diferente.

protected function registerRoutes($dic)
{
  $router = $dic['router'];

  $router->map(['GET','OPTIONS'],'/api/users',function($request,$response) use ($dic)
  {
    $controller = $dic['user_api_controller'];
    return $controller->findAllAction($request,$response);
  })->setName('api_users');
}

Básicamente estoy registrando una función que se ejecutará para el / api / users URI . Esta es en realidad una función de middleware que termina siendo almacenada en una pila. Otras funciones se envolverán alrededor de él. Muy parecido a Node.js / Express.js hace.

El contenedor inyección de dependencia está disponible (a través de la cláusula de uso) dentro de la función cuando se llama. Es posible hacer algún tipo de clase de acción de ruta, pero este código resulta ser más simple, más rápido y más fácil de mantener.

    
respondido por el Cerad 05.06.2015 - 23:37
-1

Un cierre es una pieza de código arbitrario, incluidas las variables, que se puede manejar como datos de primera clase.

Un ejemplo trivial es bueno qsort antiguo: es una función para ordenar datos. Tienes que darle un puntero a una función que compara dos objetos. Así que tienes que escribir una función. Es posible que esa función deba parametrizarse, lo que significa que le asigna variables estáticas. Lo que significa que no es seguro para subprocesos. Estás en DS. Entonces, escribe una alternativa que toma un cierre en lugar de un puntero a función. Resuelve instantáneamente el problema de la parametrización porque los parámetros se convierten en parte del cierre. Usted hace que su código sea más legible porque escribe cómo se comparan los objetos directamente con el código que llama a la función de clasificación.

Hay un montón de situaciones en las que desea realizar alguna acción que requiera una buena cantidad de código de placa de caldera, además de una pieza pequeña pero esencial de código que deba adaptarse. Para evitar el código de repetición, escriba una función una vez que toma un parámetro de cierre y hace todo el código de repetición alrededor de él, y luego puede llamar a esta función y pasar el código para que se adapte como un cierre. Una forma muy compacta y legible de escribir código.

Tiene una función en la que es necesario realizar algún código no trivial en muchas situaciones diferentes. Esto solía producir duplicación de código o código contorsionado para que el código no trivial estuviera presente solo una vez. Trivial: asigna un cierre a una variable y la llama de la manera más obvia donde sea necesario.

Multithreading: iOS / MacOS X tiene funciones para hacer cosas como "realizar este cierre en un hilo de fondo", "... en el hilo principal", "... en el hilo principal, dentro de 10 segundos". Hace que multihilo trivial .

Llamadas asíncronas: eso es lo que vio el OP. Cualquier llamada que acceda a Internet, o cualquier otra cosa que pueda tomar tiempo (como la lectura de coordenadas GPS) es algo en lo que no puede esperar el resultado. Así que tienes funciones que hacen cosas en segundo plano, y luego pasas un cierre para decirles qué hacer cuando terminen.

Es un pequeño comienzo. Cinco situaciones donde los cierres son revolucionarios en términos de producir código compacto, legible, confiable y eficiente.

    
respondido por el gnasher729 13.05.2016 - 22:38
-4

Un cierre es una forma abreviada de escribir un método en el que se va a utilizar. Le ahorra el esfuerzo de declarar y escribir un método separado. Es útil cuando el método se usará solo una vez y la definición del método es corta. Los beneficios se reducen al escribir, ya que no es necesario especificar el nombre de la función, su tipo de retorno o su modificador de acceso. Además, al leer el código, no tiene que buscar en otro lugar la definición del método.

Lo anterior es un resumen de Understand Lambda Expressions de Dan Avidar.

Esto aclaró el uso de los cierres para mí porque aclara las alternativas (cierre versus método) y los beneficios de cada uno.

El siguiente código se usa una vez, y solo una vez durante la configuración. Escribirlo en su lugar bajo viewDidLoad evita el problema de buscarlo en otro lugar y acorta el tamaño del código.

myPhoton!.getVariable("Temp", completion: { (result:AnyObject!, error:NSError!) -> Void in
  if let e = error {
    self.getTempLabel.text = "Failed reading temp"
  } else {
    if let res = result as? Float {
    self.getTempLabel.text = "Temperature is \(res) degrees"
    }
  }
})

Además, proporciona un proceso asíncrono para completarse sin bloquear otras partes del programa, y, un cierre conservará un valor para reutilizarlo en llamadas de función subsiguientes.

Otro cierre; este captura un valor ...

let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)
    
respondido por el Bendrix 17.06.2015 - 19:22

Lea otras preguntas en las etiquetas

Comentarios Recientes

Algunos sistemas ignorarían los cierres. Incluso dentro de los sistemas, es posible que desee hacer algo diferente para cada cierre. No tenga miedo de usar ningún cierre que se le ocurra. <| Endoftext |> Cuando saca $ 100,000 en ocho rondas de póker un sábado de octubre y gana solo seis, no es realmente una broma. tiene que hacer para raspar la parte inferior de ese último. Si ha ganado $ 100,000 en ocho rondas de póker, y tiene tres manos perdedoras, todavía le quedan más cartas al final de todo. Este fin de... Lee mas