Perché un programma dovrebbe utilizzare una chiusura?

54

Dopo aver letto molti post che spiegano chiusure qui mi manca ancora un concetto chiave: Perché scrivere una chiusura? Quale compito specifico dovrebbe svolgere un programmatore che potrebbe essere meglio servito da una chiusura?

Esempi di chiusure in Swift sono accessi di un NSUrl e utilizzo del geocoder invertito. Ecco uno di questi esempi. Purtroppo, questi corsi presentano solo la chiusura; non spiegano perché la soluzione del codice è scritta come chiusura.

Un esempio di un problema di programmazione mondo reale che potrebbe scatenare il mio cervello per dire "aha, dovrei scrivere una chiusura per questo", sarebbe più informativo di una discussione teorica. Non c'è carenza di discussioni teoriche disponibili su questo sito.

    
posta Bendrix 05.06.2015 - 15:36
fonte

10 risposte

31

Prima di tutto, non c'è nulla di impossibile senza usare chiusure. Puoi sempre sostituire una chiusura con un oggetto che implementa un'interfaccia specifica. È solo una questione di brevità e di accoppiamento ridotto.

In secondo luogo, tieni presente che le chiusure sono spesso usate in modo inappropriato, dove un semplice riferimento di funzione o altro costrutto sarebbe più chiaro. Non dovresti prendere ogni esempio che vedi come best practice.

Dove le chiusure brillano davvero su altri costrutti è quando si usano le funzioni di ordine superiore, quando in realtà è necessario per comunicare lo stato, e si può fare un one-liner, come in questo esempio JavaScript dal pagina di wikipedia sulle chiusure :

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

Qui, threshold è molto sinteticamente e naturalmente comunicato da dove è definito dove è usato. Il suo scopo è limitato al minimo possibile. filter non deve essere scritto per consentire la possibilità di passare i dati definiti dal cliente come una soglia. Non dobbiamo definire alcuna struttura intermedia al solo scopo di comunicare la soglia in questa piccola funzione. È completamente autonomo.

Puoi puoi scrivere questo senza una chiusura, ma richiederà molto più codice ed essere più difficile da seguire. Inoltre, JavaScript ha una sintassi lambda abbastanza dettagliata. Ad esempio, in Scala, l'intero corpo della funzione sarà:

bookList filter (_.sales >= threshold)

Se puoi comunque utilizzare ECMAScript 6 , grazie a funzioni fat arrow anche il codice JavaScript diventa molto più semplice e può essere effettivamente messo su una singola riga.

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

Nel tuo codice, cerca i luoghi in cui generi un sacco di dati per comunicare i valori temporanei da un luogo a un altro. Queste sono ottime opportunità per prendere in considerazione la possibilità di sostituire con una chiusura.

    
risposta data 16.06.2015 - 19:41
fonte
49

A titolo di spiegazione, prenderò in prestito del codice da questo eccellente post sul blog sulle chiusure . È JavaScript, ma questa è la lingua che la maggior parte dei post del blog parla di chiusure, perché le chiusure sono così importanti in JavaScript.

Diciamo che volevi rendere un array come una tabella HTML. Puoi farlo in questo modo:

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;
}

Ma sei in balia di JavaScript per quanto riguarda il rendering di ogni elemento dell'array. Se si desidera controllare il rendering, è possibile eseguire questa operazione:

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;
}

E ora puoi semplicemente passare una funzione che restituisce il rendering che desideri.

Che cosa succede se si desidera visualizzare un totale parziale in ogni riga della tabella? Avresti bisogno di una variabile per tracciare quel totale, non è vero? Una chiusura ti consente di scrivere una funzione di rendering che si chiude sulla variabile totale parziale e ti permette di scrivere un renderer che può tenere traccia del totale parziale:

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

La magia che sta succedendo qui è che renderInt mantiene l'accesso alla variabile total , anche se renderInt viene ripetutamente chiamato ed esce.

In un linguaggio più tradizionalmente orientato agli oggetti di JavaScript, puoi scrivere una classe che contiene questa variabile totale e passarla invece di creare una chiusura. Ma una chiusura è un modo molto più potente, pulito ed elegante di farlo.

Ulteriori letture

risposta data 05.06.2015 - 16:27
fonte
22

Lo scopo di closures è semplicemente quello di preserve ; da qui il nome closure - esso chiude su stato. Per la facilità di ulteriori spiegazioni, userò Javascript.

In genere hai una funzione

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

dove l'ambito della variabile (o delle variabili) è associato a questa funzione. Quindi dopo l'esecuzione la variabile txt esce dall'ambito. Non c'è modo di accedervi o utilizzarlo dopo che la funzione ha terminato l'esecuzione.

Le chiusure sono costrutti linguistici, che consentono - come detto prima - di preservare lo stato delle variabili e quindi prolungare l'ambito.

Questo potrebbe essere utile in diversi casi. Un caso d'uso è la costruzione di funzioni di ordine superiore .

In mathematics and computer science, a higher-order function (also functional form, functional or functor) is a function that does at least one of the following:1

  • takes one or more functions as an input
  • outputs a function

Un esempio semplice, ma non del tutto utile è:

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

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

Definisci una funzione makedadder , che accetta un parametro come input e restituisce una funzione . Esiste una funzione esterna function(a){} e una interna function(b){}{} . Inoltre, definisci (implicitamente) un'altra funzione add5 come risultato della chiamata della funzione di ordine superiore% codice%. makeadder restituisce una funzione anonima ( interiore ), che a sua volta assume 1 parametro e restituisce la somma del parametro della funzione esterna e il parametro di inner .

Il trucco è che, mentre si restituisce la funzione interiore , che fa l'aggiunta effettiva, l'ambito del parametro della funzione esterna ( makeadder(5) ) è conservato . a ricorda , che il parametro add5 era a .

O per mostrarne uno almeno in qualche modo utile:

  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"))));

Un altro usecase comune è il cosiddetto IIFE = espressione della funzione immediatamente invocata. È molto comune in javascript alle false variabili dei membri privati. Questo viene fatto tramite una funzione, che crea un private scope = 5 , perché è immediatamente dopo la definizione richiamata. La struttura è closure . Notare le parentesi function(){}() dopo la definizione. Ciò rende possibile utilizzarlo per la creazione di oggetti con modello di modulo rivelatore . Il trucco sta nel creare un ambito e restituire un oggetto, che ha accesso a questo ambito dopo l'esecuzione dell'IFE.

L'esempio di Addi ha questo aspetto:

 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" );

L'oggetto restituito ha riferimenti a funzioni (ad esempio () ), che a loro volta hanno accesso alle variabili "private" publicSetName .

Ma questi sono casi d'uso più speciali per Javascript.

What specific task would a programmer be performing that might be best served by a closure?

Ci sono diversi motivi per questo. Uno potrebbe essere, che è naturale per lui, dal momento che segue un paradigma funzionale . O in Javascript: è semplicemente necessario fare affidamento sulle chiusure per eludere alcuni aspetti della lingua.

    
risposta data 05.06.2015 - 19:20
fonte
16

Esistono due casi d'uso principali per le chiusure:

  1. Asynchrony. Diciamo che vuoi eseguire un'attività che richiederà un po 'di tempo, e poi fare qualcosa quando è fatta. Puoi rendere il tuo codice in attesa che sia fatto, che blocca l'ulteriore esecuzione e può rendere il tuo programma insensibile, o chiamare il tuo task in modo asincrono e dire "iniziare questa lunga attività in background, e al termine, eseguire questa chiusura", dove la chiusura contiene il codice da eseguire quando è terminato.

  2. Callback. Questi sono anche noti come "delegati" o "gestori di eventi" a seconda della lingua e della piattaforma. L'idea è di avere un oggetto personalizzabile che, in determinati punti ben definiti, eseguirà un evento , che esegue una chiusura passata dal codice che la imposta. Ad esempio, nell'interfaccia utente del tuo programma potresti avere un pulsante e gli dai una chiusura che trattiene il codice da eseguire quando l'utente fa clic sul pulsante.

Ci sono molti altri usi per le chiusure, ma quelle sono le due principali.

    
risposta data 05.06.2015 - 16:09
fonte
13

Un paio di altri esempi:

Ricerca
La maggior parte delle funzioni di ordinamento funziona confrontando coppie di oggetti. È necessaria una tecnica di confronto. Limitare il confronto a un operatore specifico significa un tipo piuttosto inflessibile. Un approccio molto migliore è quello di ricevere una funzione di confronto come argomento per la funzione di ordinamento. A volte una funzione di confronto stateless funziona bene (ad esempio, ordinando una lista di numeri o nomi), ma cosa succede se il confronto ha bisogno di stato?

Ad esempio, considera l'ordinamento di un elenco di città in base alla distanza in un luogo specifico. Una brutta soluzione è quella di memorizzare le coordinate di quella posizione in una variabile globale. Ciò rende la funzione di confronto stessa stateless, ma al costo di una variabile globale.

Questo approccio preclude il fatto di avere più thread contemporaneamente che ordinano lo stesso elenco di città in base alla loro distanza da due posizioni diverse. Una chiusura che racchiude la posizione risolve questo problema e elimina la necessità di una variabile globale.


Numeri casuali
Il% originale% co_de non ha preso argomenti. I generatori di numeri pseudocasuali hanno bisogno di stato. Alcuni (ad esempio, Mersenne Twister) hanno bisogno di molto stato. Anche il semplice ma terribile rand() ha bisogno di stato. Leggi un giornale di matematica su un nuovo generatore di numeri casuali e vedrai inevitabilmente le variabili globali. Questo è bello per gli sviluppatori della tecnica, non così bello per i chiamanti. Incapsulare quello stato in una struttura e passare la struttura al generatore di numeri casuali è un modo per aggirare il problema dei dati globali. Questo è l'approccio utilizzato in molte lingue non OO per creare un rientro del generatore di numeri casuali. Una chiusura nasconde quello stato dal chiamante. Una chiusura offre la semplice sequenza chiamante di rand() e la rientranza dello stato incapsulato.

C'è di più nei numeri casuali di un semplice PRNG. La maggior parte delle persone che vogliono la casualità vuole che sia distribuita in un certo modo. Inizierò con numeri estratti a caso tra 0 e 1 o U (0,1) in breve. Qualunque PRNG che generi interi compresi tra 0 e un massimo farà; semplicemente dividi (come un punto fluttuante) il numero intero casuale del massimo. Un modo conveniente e generico per implementare questo è creare una chiusura che richieda una chiusura (il PRNG) e il massimo come input. Ora abbiamo un generatore casuale generico e facile da usare per U (0,1).

Ci sono un certo numero di altre distribuzioni oltre a U (0,1). Ad esempio, una distribuzione normale con una determinata media e deviazione standard. Ogni normale algoritmo di generatore di distribuzione che ho incontrato utilizza un generatore U (0,1). Un modo comodo e generico per creare un generatore normale è creare una chiusura che incapsula il generatore di U (0,1), la media e la deviazione standard come stato. Questa è, almeno concettualmente, una chiusura che prende una chiusura che prende una chiusura come argomento.

    
risposta data 05.06.2015 - 22:37
fonte
7

Le chiusure sono equivalenti agli oggetti che implementano un metodo run () e, inversamente, gli oggetti possono essere emulati con chiusure.

  • Il vantaggio delle chiusure è che possono essere utilizzate facilmente ovunque ci si aspetti da una funzione: a.k.a funzioni di ordine superiore, semplici callback (o Pattern di strategia). Non è necessario definire un'interfaccia / classe per creare chiusure ad hoc.

  • Il vantaggio degli oggetti è la possibilità di avere interazioni più complesse: più metodi e / o interfacce diverse.

Quindi, usare la chiusura o gli oggetti è principalmente una questione di stile. Ecco un esempio di cose che le chiusure rendono facile ma è scomodo da implementare con gli oggetti:

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

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

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

Fondamentalmente, incapsuli uno stato nascosto a cui si accede solo tramite chiusure globali: non è necessario fare riferimento a nessun oggetto, ma utilizzare solo il protocollo definito dalle tre funzioni.

Mi fido del primo commento del supercat sul fatto che in alcune lingue è possibile controllare con precisione la durata degli oggetti, mentre la stessa cosa non è vera per le chiusure. Nel caso dei linguaggi raccolti con garbage, tuttavia, la durata degli oggetti è in genere illimitata ed è quindi possibile creare una chiusura che potrebbe essere chiamata in un contesto dinamico in cui non dovrebbe essere chiamata (lettura da una chiusura dopo un flusso è chiuso, per esempio).

Tuttavia, è abbastanza semplice prevenire tale abuso catturando una variabile di controllo che proteggerà l'esecuzione di una chiusura. Più precisamente, ecco quello che ho in mente (in Common Lisp):

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

Qui, prendiamo una funzione designatore function e restituiamo due chiusure, entrambe catturano una variabile locale chiamata active :

  • il primo delegato a function , solo quando active è true
  • il secondo imposta action su nil , a.k.a. false .

Invece di (when active ...) , è ovviamente possibile avere un'espressione (assert active) , che potrebbe generare un'eccezione nel caso in cui la chiusura venga chiamata quando non dovrebbe essere. Inoltre, tieni presente che il codice non sicuro potrebbe già generare un'eccezione da solo se usato male, quindi raramente hai bisogno di un tale wrapper.

Ecco come lo useresti:

(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))))

Si noti che le chiusure disattivanti potrebbero essere date anche ad altre funzioni; qui, le variabili locali active non sono condivise tra f e g ; inoltre, oltre a active , f si riferisce solo a obj1 e g si riferisce solo a obj2 .

L'altro punto menzionato da Supercat è che le chiusure possono portare a perdite di memoria, ma sfortunatamente, è il caso di quasi tutto negli ambienti raccolti dai rifiuti. Se sono disponibili, questo può essere risolto con indicatori deboli (la chiusura stessa potrebbe essere conservata in memoria, ma non impedisce la raccolta dei rifiuti di altre risorse).

    
risposta data 05.06.2015 - 19:30
fonte
6

Nulla di ciò che non è stato già detto, ma forse un esempio più semplice.

Ecco un esempio di JavaScript che utilizza i timeout:

// 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);
}

Quello che succede qui è che quando viene chiamata delayedLog() , torna immediatamente dopo aver impostato il timeout e il timeout continua a scorrere in background.

Ma quando il timeout si esaurisce e chiama la funzione fire() , la console visualizzerà il message che era stato originariamente passato a delayedLog() , perché era ancora disponibile a fire() tramite chiusura. Puoi chiamare delayedLog() quanto vuoi, con un messaggio diverso e ritardare ogni volta, e farà la cosa giusta.

Ma immaginiamo che JavaScript non abbia chiusure.

Un modo sarebbe rendere il blocco di setTimeout() - più simile a una funzione di "sospensione" - quindi l'ambito di delayedLog() non scompare prima che il timeout sia scaduto. Ma bloccare tutto non è molto bello.

Un altro modo sarebbe mettere la variabile message in qualche altro ambito che sarà accessibile dopo che l'estensione di delayedLog() è scomparsa.

Potresti usare variabili globali - o almeno "più ampie" - ma dovresti capire come tenere traccia di quale messaggio va con quale timeout. Ma non può essere solo una coda FIFO sequenziale, perché puoi impostare qualsiasi ritardo tu voglia. Quindi potrebbe essere "primo dentro, terzo fuori" o qualcosa del genere. Quindi avrai bisogno di altri mezzi per legare una funzione temporizzata alle variabili di cui ha bisogno.

È possibile creare un'istanza di un oggetto di timeout che "raggruppa" il timer con il messaggio. Il contesto di un oggetto è più o meno un oggetto che si aggira intorno. Quindi avresti eseguito il timer nel contesto dell'oggetto, in modo da avere accesso al messaggio giusto. Ma dovresti archiviare quell'oggetto perché senza riferimenti si otterrebbe la raccolta dei dati inutili (senza chiusure, non ci sarebbero nemmeno riferimenti impliciti). E dovresti rimuovere l'oggetto una volta che ha sparato il suo timeout, altrimenti rimarrà bloccato. Quindi avresti bisogno di una sorta di elenco di oggetti di timeout, e controllalo periodicamente per rimuovere gli oggetti "spesi", altrimenti gli oggetti si aggiungerebbero e si rimuovono dall'elenco e ...

Quindi ... sì, questo sta diventando noioso.

Per fortuna, non è necessario utilizzare un ambito più ampio o oggetti wrangle solo per mantenere determinate variabili. Poiché JavaScript ha chiusure, hai già esattamente il campo di applicazione di cui hai bisogno. Un ambito che ti dà accesso alla variabile message quando ne hai bisogno. E a causa di ciò, puoi fare a meno di scrivere delayedLog() come sopra.

    
risposta data 06.06.2015 - 13:25
fonte
3

PHP può essere usato per aiutare a mostrare un esempio reale in una lingua diversa.

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');
}

Quindi in pratica sto registrando una funzione che verrà eseguita per / api / users URI . Questa è in realtà una funzione middleware che finisce per essere archiviata in una pila. Altre funzioni saranno avvolte intorno ad esso. Praticamente come Node.js / Express.js fa.

Il contenitore iniezione di dipendenza è disponibile (tramite la clausola use) all'interno della funzione quando viene chiamato. È possibile creare una sorta di classe di azioni di percorso, ma questo codice risulta essere più semplice, più rapido e più facile da mantenere.

    
risposta data 05.06.2015 - 23:37
fonte
-1

Una chiusura è una parte di codice arbitrario, comprese le variabili, che può essere gestita come dati di prima classe.

Un esempio banale è il buon vecchio qsort: è una funzione per ordinare i dati. Devi dargli un puntatore a una funzione che confronta due oggetti. Quindi devi scrivere una funzione. Potrebbe essere necessario parametrizzare quella funzione, il che significa che le si assegnano variabili statiche. Il che significa che non è thread-safe. Sei in DS. Quindi scrivi un'alternativa che prende una chiusura invece di un puntatore a funzione. Risolvi istantaneamente il problema della parametrizzazione perché i parametri diventano parte della chiusura. Rendi il tuo codice più leggibile perché scrivi come gli oggetti vengono confrontati direttamente con il codice che chiama la funzione di ordinamento.

Ci sono un sacco di situazioni in cui si desidera eseguire un'azione che richiede una buona quantità di codice della piastra della caldaia, più un piccolo ma essenziale codice che deve essere adattato. Si evita il codice boilerplate scrivendo una funzione una volta che accetta un parametro di chiusura e fa tutto il codice di zona che lo circonda, quindi è possibile chiamare questa funzione e passare il codice da adattare come chiusura. Un modo molto compatto e leggibile per scrivere codice.

Hai una funzione in cui un codice non banale deve essere eseguito in molte situazioni diverse. Questo era usato per produrre codice duplicato o codice contorto in modo che il codice non banale fosse presente una sola volta. Trivial: assegni una chiusura a una variabile e la chiami nel modo più ovvio ovunque sia necessario.

Multithreading: iOS / MacOS X ha funzioni per fare cose come "eseguire questa chiusura su un thread in background", "... sul thread principale", "... sul thread principale, 10 secondi da ora". Rende il banale multithreading.

Chiamate asincrone: ecco cosa vide l'OP. Qualsiasi chiamata che accede a Internet o qualsiasi altra cosa che potrebbe richiedere del tempo (come la lettura delle coordinate GPS) è qualcosa in cui non si può attendere il risultato. Quindi hai delle funzioni che fanno le cose in background, e poi passi una chiusura per dire loro cosa fare quando hanno finito.

È un piccolo inizio. Cinque situazioni in cui le chiusure sono rivoluzionarie in termini di produzione di codice compatto, leggibile, affidabile ed efficiente.

    
risposta data 13.05.2016 - 22:38
fonte
-4

Una chiusura è un modo stenografico per scrivere un metodo in cui deve essere usato. Ti risparmia lo sforzo di dichiarare e scrivere un metodo separato. È utile quando il metodo verrà usato una sola volta e la definizione del metodo è breve. I vantaggi sono ridotti digitando non c'è bisogno di specificare il nome della funzione, il suo tipo di ritorno o il suo modificatore di accesso. Inoltre, durante la lettura del codice non è necessario cercare altrove la definizione del metodo.

Quanto sopra è un riassunto di Understand Lambda Expressions di Dan Avidar.

Questo ha chiarito l'uso delle chiusure perché chiarisce le alternative (chiusura vs. metodo) e i benefici di ciascuna.

Il seguente codice viene utilizzato una volta e solo una volta durante l'installazione. Scrivendolo in posizione sotto viewDidLoad risparmia la fatica di cercarlo altrove e riduce la dimensione del codice.

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"
    }
  }
})

Inoltre, consente di completare un processo asincrono senza bloccare altre parti del programma e, una chiusura manterrà un valore per il riutilizzo nelle successive chiamate di funzione.

Un'altra chiusura; questo cattura un valore ...

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