Pourquoi un programme utiliserait-il une fermeture?

58

Après avoir lu de nombreux articles expliquant les fermetures ici, il me manque encore un concept clé: Pourquoi écrire une fermeture? Quelle tâche spécifique un programmeur effectuerait-il et qui pourrait être mieux servi par une fermeture?

Des exemples de fermetures dans Swift sont les accès d’un NSUrl et l’utilisation du géocodeur inversé. Voici un exemple. Malheureusement, ces cours ne font que présenter la clôture; ils n'expliquent pas pourquoi la solution de code est écrite comme une fermeture.

Un exemple de problème de programmation dans le monde réel qui pourrait amener mon cerveau à dire «aha, je devrais écrire une clôture pour cela» serait plus informatif qu’une discussion théorique. Les discussions théoriques disponibles sur ce site ne manquent pas.

Bendrix
la source
7
"Exlanation récemment revue des fermetures ici" - il vous manque un lien?
Dan Pichelman
2
Vous devriez mettre cette clarification sur la tâche asynchrone dans un commentaire, sous la réponse appropriée. Cela ne fait pas partie de votre question initiale et le répondeur ne sera jamais informé de votre modification.
Robert Harvey
5
Juste un petit détail: le point crucial des fermetures est la portée lexicale (par opposition à la portée dynamique), les fermetures sans portée lexicale sont un peu inutiles. Les fermetures sont parfois appelées fermetures lexicales.
Hoffmann
1
Les fermetures vous permettent d’ instancier des fonctions .
user253751
1
Lisez n'importe quel bon livre sur la programmation fonctionnelle. Peut-être commencer par SICP
Basile Starynkevitch le

Réponses:

33

Tout d'abord, il n'y a rien d'impossible sans l'utilisation de fermetures. Vous pouvez toujours remplacer une fermeture par un objet implémentant une interface spécifique. Ce n'est qu'une question de brièveté et de couplage réduit.

Deuxièmement, gardez à l'esprit que les fermetures sont souvent utilisées de manière inappropriée, où une simple référence de fonction ou une autre construction serait plus claire. Vous ne devriez pas prendre tous les exemples que vous voyez comme une pratique exemplaire.

Les fermetures brillent vraiment par rapport aux autres constructions lorsque vous utilisez des fonctions d'ordre supérieur, lorsque vous avez réellement besoin de communiquer l'état et que vous pouvez en faire une ligne, comme dans cet exemple JavaScript de la page wikipedia sur les fermetures :

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

Ici, thresholdest très succinctement et naturellement communiqué de l'endroit où il est défini à l'endroit où il est utilisé. Sa portée est précisément limitée au minimum. filterIl n'est pas nécessaire d'écrire pour permettre la possibilité de transmettre des données définies par le client comme un seuil. Nous n'avons pas à définir de structures intermédiaires dans le seul but de communiquer le seuil dans cette seule petite fonction. C'est entièrement autonome.

Vous pouvez écrire ceci sans fermeture, mais cela nécessitera beaucoup plus de code et sera plus difficile à suivre. En outre, JavaScript a une syntaxe lambda assez détaillée. En Scala, par exemple, le corps entier de la fonction serait:

bookList filter (_.sales >= threshold)

Si vous pouvez cependant utiliser ECMAScript 6 , grâce aux fonctions de flèche épaisse, même le code JavaScript devient beaucoup plus simple et peut être mis sur une seule ligne.

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

Dans votre propre code, recherchez les endroits où vous générez beaucoup de passe-passe pour communiquer des valeurs temporaires d'un endroit à un autre. Ce sont d'excellentes opportunités à envisager de remplacer par une fermeture.

Karl Bielefeldt
la source
Comment exactement les fermetures réduisent-elles le couplage?
Sichicho
Sans fermeture, vous devez imposer des restrictions à la fois sur le bestSellingBookscode et sur le filtercode, telles qu'une interface spécifique ou un argument de données utilisateur, pour pouvoir communiquer les thresholddonnées. Cela lie les deux fonctions de manière beaucoup moins réutilisable.
Karl Bielefeldt le
51

En guise d’explication, je vais emprunter du code de cet excellent article de blog sur les fermetures . C'est du JavaScript, mais c'est le langage que la plupart des blogs qui parlent de fermetures utilisent, car les fermetures sont si importantes en JavaScript.

Supposons que vous vouliez rendre un tableau sous forme de tableau HTML. Vous pouvez le faire comme ceci:

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

Mais vous êtes à la merci de JavaScript quant à la manière dont chaque élément du tableau sera rendu. Si vous voulez contrôler le rendu, vous pouvez faire ceci:

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

Et maintenant, vous pouvez simplement passer une fonction qui retourne le rendu souhaité.

Que faire si vous souhaitez afficher un total cumulé dans chaque ligne du tableau? Vous auriez besoin d'une variable pour suivre ce total, n'est-ce pas? Une fermeture vous permet d'écrire une fonction de rendu qui se ferme sur la variable du total en cours et vous permet d'écrire un moteur de rendu capable de suivre le total en cours:

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

La magie qui se passe ici est que l’ accès à la variable est renderInt conservé total, même s’il renderIntest appelé et sorti à plusieurs reprises.

Dans un langage plus traditionnellement orienté objet que JavaScript, vous pouvez écrire une classe contenant cette variable totale et la transmettre au lieu de créer une fermeture. Mais une fermeture est une manière beaucoup plus puissante, propre et élégante de le faire.

Lectures complémentaires

Robert Harvey
la source
10
En général, vous pouvez dire que "fonction de première classe == objet avec une seule méthode", "objet de clôture == avec une seule méthode et état", "objet == ensemble de fermetures".
Jörg W Mittag
Cela semble bon. Une autre chose, et il s’agit peut-être de pédantisme, est que Javascript est orienté objet et que vous pouvez créer un objet contenant la variable totale et le transmettre. Les fermetures restent beaucoup plus puissantes, propres et élégantes, et permettent également un Javascript beaucoup plus idiomatique, mais la façon dont il est écrit pourrait impliquer que Javascript ne peut pas le faire de manière orientée objet.
Kryan
2
En fait, pour être vraiment pédant: les fermetures sont ce qui rend JavaScript en premier lieu orienté objet! OO concerne l'abstraction de données, et les fermetures constituent le moyen d'effectuer cette abstraction en JavaScript.
Jörg W Mittag
@ JörgWMittag: Comme vous pouvez voir les objets en utilisant une seule méthode, vous pouvez voir les objets en tant que collection de fermetures se fermant sur les mêmes variables: les variables membres d'un objet ne sont que les variables locales du constructeur de l'objet, et les méthodes de l'objet sont des fermetures définies dans la portée du constructeur et mises à disposition pour être appelées ultérieurement. La fonction constructeur renvoie une fonction d'ordre supérieur (l'objet) qui peut être distribuée à chaque fermeture en fonction du nom de la méthode utilisée pour l'appel.
Giorgio
@Giorgio: En effet, c’est ainsi que les objets sont généralement implémentés dans Scheme, et c’est également ainsi que les objets sont implémentés en JavaScript (ce qui n’est pas surprenant compte tenu de sa relation étroite avec Scheme. Après tout, Brendan Eich a été embauché à l’origine pour concevoir Schéma dialect et implémenter un interpréteur Scheme intégré dans Netscape Navigator, et ce n’est que plus tard qu’il a été ordonné de créer un langage "avec des objets qui ressemblent à C ++", après quoi il a apporté le minimum absolu de modifications pour se conformer à ces exigences marketing).
Jörg W Mittag
23

Le but de closuresest simplement de préserver l' état; d'où le nom closure- il ferme sur l'état. Pour faciliter l'explication, j'utilise le langage Javascript.

Vous avez généralement une fonction

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

où la portée de la ou des variables est liée à cette fonction. Donc, après exécution, la variable txtsort de la portée. Il n'y a aucun moyen d'y accéder ou de l'utiliser une fois l'exécution de la fonction terminée.

Les fermetures sont des constructions de langage, qui permettent - comme dit précédemment - de conserver l’état des variables et donc de prolonger la portée.

Cela pourrait être utile dans différents cas. Un cas d'utilisation est la construction de fonctions d'ordre supérieur .

En mathématiques et en informatique, une fonction d'ordre supérieur (également forme fonctionnelle, fonctionnelle ou foncteur) est une fonction qui effectue au moins l'une des opérations suivantes: 1

  • prend une ou plusieurs fonctions en entrée
  • sort une fonction

Voici un exemple simple, mais pas trop utile:

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

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

Vous définissez une fonction makedadderqui prend un paramètre en entrée et renvoie une fonction . Il existe une fonction externefunction(a){} et une fonction interne. En function(b){}{} outre, vous définissez (implicitement) une autre fonction add5à la suite de l'appel de la fonction d'ordre supérieur makeadder. makeadder(5)renvoie une fonction anonyme ( interne ), qui prend à son tour 1 paramètre et renvoie la somme du paramètre de la fonction externe et du paramètre de la fonction interne .

L' astuce est que, tout en renvoyant la fonction interne , qui effectue l'addition réelle, la portée du paramètre de la fonction externe ( a) est préservée. add5 se souvient que le paramètre aétait 5.

Ou pour montrer au moins un exemple 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"))));

Une autre utilisation courante est la dénommée IIFE = expression de fonction immédiatement appelée. Il est très fréquent en javascript pour faux variables membres privées. Cela se fait via une fonction, qui crée une portée privée = closure, car elle est immédiatement après la définition invoquée. La structure est function(){}(). Notez les crochets ()après la définition. Cela permet de l'utiliser pour la création d'objets avec un motif de module révélateur . L'astuce consiste à créer une étendue et à renvoyer un objet, qui a accès à cette étendue après l'exécution de l'IIFE.

L'exemple d'Addi ressemble à ceci:

 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'objet retourné a des références à des fonctions (par exemple publicSetName), qui ont à leur tour accès à des variables "privées" privateVar.

Mais ce sont des cas d'utilisation plus spéciaux pour Javascript.

Quelle tâche spécifique un programmeur effectuerait-il et qui pourrait être mieux servi par une fermeture?

Il y a plusieurs raisons à cela. On pourrait dire que c'est naturel pour lui, puisqu'il suit un paradigme fonctionnel . Ou en Javascript: il est simplement nécessaire de s’appuyer sur les fermetures pour contourner certaines bizarreries du langage.

Thomas Junk
la source
"Le but des fermetures est simplement de préserver l'état; d'où le nom fermeture - il ferme sur état.": Cela dépend de la langue, vraiment. Les fermetures se ferment sur les noms / variables externes. Ceux-ci peuvent indiquer un état (emplacements mémoire pouvant être modifiés), mais ils peuvent également indiquer des valeurs (immuables). Les fermetures dans Haskell ne préservent pas l’état: elles préservent les informations connues à l’heure actuelle et dans le contexte dans lequel la fermeture a été créée.
Giorgio
1
»Ils conservent des informations connues à l’heure actuelle et dans le contexte dans lequel la fermeture a été créée« indépendamment du fait qu’elle puisse être modifiée ou non, c’est un état - peut-être un état immuable .
Thomas
16

Il existe deux principaux cas d'utilisation des fermetures:

  1. Asynchronisme. Supposons que vous souhaitiez effectuer une tâche qui prendrait un certain temps, puis que vous agissiez ensuite. Vous pouvez soit faire attendre votre code, ce qui bloque toute exécution ultérieure et empêcher votre programme de répondre, ou appeler votre tâche de manière asynchrone et dire "commencez cette longue tâche en arrière-plan et, une fois terminé, exécutez cette fermeture", où la fermeture contient le code à exécuter quand c'est fait.

  2. Rappels. Ceux-ci sont également appelés "délégués" ou "gestionnaires d'événements" en fonction de la langue et de la plate-forme. L'idée est que vous avez un objet personnalisable qui, à certains points bien définis, exécutera un événement , qui exécute une fermeture transmise par le code qui le configure. Par exemple, dans l'interface utilisateur de votre programme, vous pouvez avoir un bouton et lui donner une fermeture qui contient le code à exécuter lorsque l'utilisateur clique sur le bouton.

Les fermetures ont plusieurs autres utilisations, mais ce sont les deux principales.

Maçon Wheeler
la source
23
Donc, essentiellement des rappels alors, puisque le premier exemple est aussi un rappel.
Robert Harvey
2
@ RobertHarvey: Techniquement vrai, mais ce sont des modèles mentaux différents. Par exemple, (de manière générale), vous vous attendez à ce que le gestionnaire d'événements soit appelé plusieurs fois, mais votre continuation async ne soit appelée qu'une fois. Mais oui, techniquement, tout ce que vous feriez avec une fermeture est un rappel. (Sauf si vous le convertissez en un arbre d'expression, mais c'est une question complètement différente.);)
Mason Wheeler
4
@RobertHarvey: L'affichage des fermetures en tant que rappels vous place dans un état d'esprit qui vous empêchera vraiment de les utiliser efficacement. Les fermetures sont des rappels sur les stéroïdes.
gnasher729
13
@MasonWheeler Dit de cette façon, ça sonne mal. Les rappels n'ont rien à voir avec les fermetures; bien sûr, vous pouvez rappeler une fonction dans une fermeture ou appeler une fermeture en tant que rappel; mais un rappel n'est pas nécessaire une fermeture. Un rappel n'est qu'un simple appel de fonction. Un gestionnaire d'événements n'est pas en soi une fermeture. Un délégué n'est pas nécessairement une fermeture: il s'agit principalement des pointeurs de fonction C #. Bien sûr, vous pouvez l'utiliser pour construire une fermeture. Votre explication est imprécise. Le point est de fermer au-dessus de l'état et de l'utiliser.
Thomas Junk
5
Peut-être y a-t-il des connotations spécifiques à JS ici qui me font mal comprendre, mais cette réponse me semble absolument fausse. L'asynchronisme ou les rappels peuvent utiliser tout ce qui est appelable. Une fermeture n'est pas nécessaire pour l'un ou l'autre - une fonction régulière fera l'affaire. Pendant ce temps, une fermeture, telle que définie dans la programmation fonctionnelle, ou telle qu'elle est utilisée dans Python, est utile, comme le dit Thomas, car elle "ferme" un certain état, c'est-à-dire une variable, et donne à une fonction dans une portée interne un accès cohérent aux données de cette variable. valeur même si la fonction est appelée et quitte plusieurs fois.
Jonathan Hartley
13

Quelques autres exemples:

Tri
La plupart des fonctions de tri fonctionnent en comparant des paires d'objets. Une technique de comparaison est nécessaire. Restreindre la comparaison à un opérateur spécifique signifie une sorte plutôt inflexible. Une meilleure approche consiste à recevoir une fonction de comparaison en tant qu'argument de la fonction de tri. Parfois, une fonction de comparaison sans état fonctionne bien (par exemple, trier une liste de nombres ou de noms), mais que se passe-t-il si la comparaison nécessite un état?

Par exemple, envisagez de trier une liste de villes en fonction de leur distance par rapport à un emplacement spécifique. Une mauvaise solution consiste à stocker les coordonnées de cet emplacement dans une variable globale. Cela rend la fonction de comparaison elle-même sans état, mais au prix d'une variable globale.

Cette approche empêche d'avoir plusieurs threads triant simultanément la même liste de villes en fonction de leur distance à deux emplacements différents. Une fermeture qui entoure l'emplacement résout ce problème et élimine le besoin d'une variable globale.


Nombres aléatoires
L'original rand()n'a pris aucun argument. Les générateurs de nombres pseudo-aléatoires ont besoin d'un état. Certains (par exemple, Mersenne Twister) ont besoin de beaucoup d’état. Même le simple mais terrible rand()état nécessaire. Lisez un article de journal de maths sur un nouveau générateur de nombres aléatoires et vous verrez inévitablement des variables globales. C'est bien pour les développeurs de la technique, pas si bien pour les appelants. Encapsuler cet état dans une structure et la transmettre au générateur de nombres aléatoires est un moyen de résoudre le problème des données globales. C'est l'approche utilisée dans de nombreux langages non-OO pour rendre un générateur de nombres aléatoires réentrant. Une fermeture cache cet état à l'appelant. Une fermeture offre la séquence d'appel simple rand()et la réentrance de l'état encapsulé.

Les nombres aléatoires ne se limitent pas à un simple PRNG. La plupart des gens qui veulent le hasard veulent qu'il soit distribué d'une certaine manière. Je commencerai par des nombres tirés au hasard, compris entre 0 et 1, ou U (0,1) en abrégé. Tout PRNG générant des entiers compris entre 0 et un maximum fera l'affaire; il suffit de diviser (en virgule flottante) l'entier aléatoire par le maximum. Une façon pratique et générique de mettre cela en œuvre consiste à créer une fermeture qui prend une fermeture (le PRNG) et le maximum en tant qu'entrées. Nous avons maintenant un générateur aléatoire générique et facile à utiliser pour U (0,1).

Il existe un certain nombre d'autres distributions en plus de U (0,1). Par exemple, une distribution normale avec un certain écart moyen et standard. Chaque algorithme de générateur de distribution normal que j'ai rencontré utilise un générateur U (0,1). Un moyen pratique et générique de créer un générateur normal consiste à créer une fermeture qui encapsule le générateur U (0,1), la moyenne et l'écart type en tant qu'état. Il s’agit, du moins sur le plan conceptuel, d’une fermeture qui prend une fermeture qui prend pour argument une fermeture.

David Hammen
la source
7

Les fermetures sont équivalentes aux objets implémentant une méthode run () et inversement, les objets peuvent être émulés avec des fermetures.

  • L'avantage des fermetures réside dans le fait qu'elles peuvent être utilisées facilement n'importe où vous attendez une fonction: alias fonctions d'ordre supérieur, rappels simples (ou modèle de stratégie). Vous n'avez pas besoin de définir une interface / classe pour créer des fermetures ad-hoc.

  • L'avantage des objets est la possibilité d'avoir des interactions plus complexes: plusieurs méthodes et / ou différentes interfaces.

Donc, utiliser des fermetures ou des objets est avant tout une question de style. Voici un exemple de choses que les fermetures facilitent mais qu'il est peu pratique de mettre en œuvre avec des objets:

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

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

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

Fondamentalement, vous encapsulez un état masqué auquel vous accédez uniquement par le biais de fermetures globales: vous n'avez pas besoin de faire référence à un objet, utilisez uniquement le protocole défini par les trois fonctions.


  • J'étends la réponse pour répondre à ce commentaire de supercat *.

Je me fie au premier commentaire de supercat sur le fait que dans certaines langues, il est possible de contrôler précisément la durée de vie des objets, alors que la même chose n'est pas vraie pour les fermetures. Cependant, dans le cas des langages collectés avec ordures, la durée de vie des objets est généralement illimitée et il est donc possible de créer une fermeture pouvant être appelée dans un contexte dynamique où elle ne devrait pas être appelée (lecture d'une fermeture après un flux). est fermé, par exemple).

Cependant, il est assez simple d'empêcher une telle utilisation abusive en capturant une variable de contrôle qui protégera l'exécution d'une fermeture. Plus précisément, voici ce que j'ai en tête (en Common Lisp):

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

Ici, nous prenons un indicateur de fonction functionet retournons deux fermetures, chacune capturant une variable locale nommée active:

  • le premier délégué à function, seulement quand activeest vrai
  • le second se met actionà nil, aka false.

Au lieu de (when active ...), il est bien sûr possible d’avoir une (assert active)expression, ce qui pourrait déclencher une exception si la fermeture est appelée alors qu’elle ne devrait pas l'être. De plus, gardez à l'esprit que le code non sécurisé peut déjà générer une exception lorsqu'il est mal utilisé. Vous avez donc rarement besoin d'un tel wrapper.

Voici comment vous l'utiliseriez:

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

Notez que les fermetures désactivantes pourraient également être attribuées à d'autres fonctions; ici, les activevariables locales ne sont pas partagées entre fet g; De plus, en plus de active, fne fait que se référer obj1et gse réfère seulement à obj2.

Supercat a également mentionné que les fermetures peuvent entraîner des fuites de mémoire, mais malheureusement, c’est le cas pour presque tout dans des environnements mal nettoyés. Si elles sont disponibles, cela peut être résolu par des indicateurs faibles (la fermeture elle-même peut être conservée en mémoire, mais n'empêche pas le ramassage des ordures d'autres ressources).

coredump
la source
1
Un inconvénient des fermetures généralement implémentées est qu’une fois qu’une fermeture qui utilise une variable locale d’une méthode est exposée au monde extérieur, la méthode qui définit la fermeture peut perdre le contrôle de la lecture et de l’écriture de cette variable. Il n’existe aucun moyen pratique, dans la plupart des langues, de définir une fermeture éphémère, de la passer à une méthode et de garantir qu’elle cessera d’exister une fois que la méthode qui la reçoit est renvoyée. garantir qu'une fermeture ne sera pas exécutée dans un contexte de thread inattendu.
Supercat
@supercat: Regardez Objective-C et Swift.
gnasher729
1
@supercat Le même inconvénient n'existe-t-il pas avec les objets avec état?
Andres F.
@AndresF .: Il est possible d'encapsuler un objet avec état dans un wrapper prenant en charge l'invalidation; si le code encapsule une propriété privée List<T>dans une (classe hypothétique) TemporaryMutableListWrapper<T>et l’expose à un code extérieur, on peut s’assurer que, s’il invalide le wrapper, le code extérieur n’aura plus aucun moyen de le manipuler List<T>. On peut concevoir des fermetures pour permettre l'invalidation une fois qu'elles ont atteint l'objectif recherché, mais ce n'est pas pratique. Les fermetures existent pour rendre certains modèles commodes, et l'effort requis pour les protéger reviendrait à nier cela.
Supercat
1
@coredump: En C #, si deux fermetures ont des variables en commun, le même objet généré par le compilateur les servira toutes les deux, car les modifications apportées à une variable partagée par une fermeture doivent être vues par l'autre. Pour éviter ce partage, il aurait fallu que chaque fermeture soit son propre objet, qui contient ses propres variables non partagées, ainsi qu'une référence à un objet partagé contenant les variables partagées. Ce n'est pas impossible, mais cela aurait ajouté un niveau supplémentaire de déréférencement à la plupart des accès variables, ce qui aurait tout ralenti.
Supercat
6

Rien de ce qui n'a pas encore été dit, mais peut-être un exemple plus simple.

Voici un exemple JavaScript utilisant des délais d'attente:

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

Ce qui se passe ici, c’est que lorsqu’on delayedLog()appelle, l’appareil revient immédiatement après avoir défini le délai d’expiration, lequel continue de s’écouler en arrière-plan.

Mais lorsque le délai imparti est épuisé et que la fire()fonction est appelée, la console affiche le texte messagequi a été initialement transmis delayedLog(), car il est toujours disponible fire()via la fermeture. Vous pouvez appeler delayedLog()autant de fois que vous le souhaitez, avec un message et un délai différents à chaque fois, et tout se passera bien.

Mais imaginons que JavaScript ne soit pas fermé.

Une solution serait de rendre le setTimeout()blocage - plutôt comme une fonction "sommeil" - afin que delayedLog()la portée ne disparaisse pas tant que le délai d’expiration n’est pas écoulé. Mais tout bloquer n'est pas très gentil.

Une autre solution consisterait à placer la messagevariable dans une autre portée qui sera accessible après delayedLog()la suppression de la portée.

Vous pouvez utiliser des variables globales - ou du moins des "portées plus larges" -, mais vous devez savoir comment garder trace de quel message va avec quel délai. Mais il ne peut pas s'agir simplement d'une file d'attente FIFO séquentielle, car vous pouvez définir le délai que vous souhaitez. Donc, cela pourrait être "premier entré, troisième sorti" ou quelque chose. Il vous faut donc un autre moyen pour lier une fonction chronométrée aux variables dont elle a besoin.

Vous pouvez instancier un objet de délai d'attente qui "regroupe" le minuteur avec le message. Le contexte d'un objet est plus ou moins une étendue qui reste. Ensuite, vous feriez exécuter la minuterie dans le contexte de l'objet afin qu'il ait accès au bon message. Mais vous devez stocker cet objet car, sans aucune référence, les ordures seraient collectées (sans les fermetures, il n'y aurait aucune référence implicite non plus). Et vous devriez retirer l'objet une fois que son délai d'attente a été déclenché, sinon il restera dans les parages. Donc, vous auriez besoin d'une sorte de liste d'objets de délai d'expiration, et vérifiez périodiquement si des objets "utilisés" ont été supprimés - ou les objets s'ajouteraient et se supprimeraient de la liste, et ...

Alors ... ouais, ça devient ennuyeux.

Heureusement, vous n'avez pas besoin d'utiliser une portée plus large, ni de démêler des objets pour conserver certaines variables. Parce que JavaScript a des fermetures, vous avez déjà exactement la portée dont vous avez besoin. Une portée qui vous donne accès à la messagevariable quand vous en avez besoin. Et à cause de cela, vous pouvez vous en tirer en écrivant delayedLog()comme ci-dessus.

Flambino
la source
J'ai du mal à appeler ça une fermeture . Peut-être que je parlerais de fermeture accidentelle . messageest inclus dans la portée de la fonction fireet est donc référencé dans les appels suivants; mais c'est accidentel pour ainsi dire. Techniquement, c'est une fermeture. +1 de toute façon;)
Thomas
@ThomasJunk Je ne suis pas sûr de bien suivre. À quoi ressemblerait une "fermeture non accidentelle"? J'ai vu votre makeadderexemple ci-dessus qui, à mes yeux, semble être à peu près le même. Vous retournez une fonction "curry" qui prend un seul argument au lieu de deux; en utilisant les mêmes moyens, je crée une fonction qui prend zéro argument. Je juste ne le retourne pas mais le passe à la setTimeoutplace.
Flambino
»Je ne le retourne tout simplement pas« peut-être, voilà le point qui fait la différence pour moi. Je ne peux pas bien exprimer ma "préoccupation";) En termes techniques, vous avez parfaitement raison. Référencer messagedans firegénère la fermeture. Et quand il est appelé, setTimeoutil utilise l'état préservé.
Thomas Junk
1
@ThomasJunk Je vois comment ça pourrait sentir un peu différent. Je ne partage peut-être pas votre inquiétude, cependant :) Ou, en tout cas, je n'appellerais pas cela un "accidentel" - je suis presque sûr d'avoir codé de cette façon exprès;)
Flambino
3

PHP peut être utilisé pour aider à montrer un exemple réel dans une langue différente.

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

Donc, fondamentalement, j'enregistre une fonction qui sera exécutée pour l’ URI / api / users . C’est en fait une fonction de middleware qui finit par être stockée sur une pile. D'autres fonctions seront enroulées autour de lui. Un peu comme Node.js / Express.js .

Le conteneur d' injection de dépendance est disponible (via la clause use) dans la fonction lorsqu'elle est appelée. Il est possible de créer une sorte de classe d'action de route, mais ce code s'avère plus simple, plus rapide et plus facile à gérer.

Cerad
la source
-1

Une fermeture est un morceau de code arbitraire, y compris des variables, pouvant être traité comme des données de première classe.

Un bon exemple trivial est le bon vieux qsort: c'est une fonction pour trier les données. Vous devez lui donner un pointeur sur une fonction qui compare deux objets. Donc, vous devez écrire une fonction. Cette fonction devra peut-être être paramétrée, ce qui signifie que vous lui attribuez des variables statiques. Ce qui signifie que ce n'est pas thread-safe. Vous êtes dans DS. Donc, vous écrivez une alternative qui prend une fermeture au lieu d'un pointeur de fonction. Vous résolvez instantanément le problème de paramétrage car les paramètres deviennent partie intégrante de la fermeture. Vous rendez votre code plus lisible car vous écrivez comment les objets sont comparés directement avec le code qui appelle la fonction de tri.

Il existe une multitude de situations dans lesquelles vous souhaitez effectuer une action nécessitant une grande partie du code de la plaque de la chaudière, plus un élément de code minuscule mais essentiel qui doit être adapté. Vous évitez le code passe-partout en écrivant une fois une fonction qui prend un paramètre de fermeture et fait tout le code autour de lui, puis vous pouvez appeler cette fonction et transmettre le code à adapter en tant que fermeture. Un moyen très compact et lisible d'écrire du code.

Vous avez une fonction dans laquelle un code non trivial doit être exécuté dans de nombreuses situations différentes. Cela produisait soit une duplication de code, soit un code contorsionné, de sorte que le code non trivial ne soit présent qu'une seule fois. Trivial: vous affectez une fermeture à une variable et vous l'appelez de la manière la plus évidente chaque fois que cela est nécessaire.

Multithreading: iOS / MacOS X dispose de fonctions telles que "effectuer cette fermeture sur un fil de fond", "... sur le fil principal", "... sur le fil principal, dans 10 secondes". Le multithreading est trivial .

Appels asynchrones: c'est ce que l'OP a vu. N'importe quel appel qui accède à Internet, ou toute autre chose qui pourrait prendre du temps (comme la lecture des coordonnées GPS) est quelque chose où vous ne pouvez pas attendre le résultat. Vous avez donc des fonctions qui font les choses en arrière-plan, puis vous passez une fermeture pour leur dire quoi faire quand elles sont finies.

C'est un petit début. Cinq situations où les fermetures sont révolutionnaires en termes de production de code compact, lisible, fiable et efficace.

gnasher729
la source
-4

Une fermeture est un moyen abrégé d'écrire une méthode où elle doit être utilisée. Cela vous évite l'effort de déclarer et d'écrire une méthode séparée. C'est utile lorsque la méthode ne sera utilisée qu'une seule fois et que la définition de la méthode est courte. Les avantages sont réduits en tapant car il n’est pas nécessaire de spécifier le nom de la fonction, son type de retour ou son modificateur d’accès. De plus, lors de la lecture du code, vous n'avez pas à chercher ailleurs la définition de la méthode.

Ce qui précède est un résumé de Understand Lambda Expressions de Dan Avidar.

Ceci a clarifié l'utilisation des fermetures pour moi parce que cela clarifie les alternatives (fermeture vs méthode) et les avantages de chacune.

Le code suivant est utilisé une fois, et une fois seulement lors de l'installation. L'écrire en place sous viewDidLoad vous évite d'avoir à le chercher ailleurs et réduit la taille du code.

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

De plus, il prévoit qu'un processus asynchrone se termine sans bloquer d'autres parties du programme et qu'une fermeture conservera une valeur à réutiliser lors d'appels de fonction ultérieurs.

Une autre fermeture; celui-ci capture une valeur ...

let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sorted({ (one: String, two: String) -> Bool in return one > two
}) println(sortedStrings)
Bendrix
la source
4
Malheureusement, cela confond les fermetures et les lambdas. Les Lambda utilisent souvent des fermetures, car elles sont généralement beaucoup plus utiles si elles sont définies a) de manière très nuancée et b) au sein d’un contexte de méthode donné, y compris des variables. Cependant, la fermeture n’a rien à voir avec l’idée d’un lambda, c’est-à-dire la possibilité de définir une fonction de première classe et de la transmettre pour une utilisation ultérieure.
Nathan Tuggy
Pendant que vous votez, répondez ceci ... Quand devrais-je voter? Utilisez vos votes négatifs à chaque fois que vous rencontrez un message extrêmement bâclé, sans effort, ou une réponse qui est clairement et peut-être dangereusement incorrecte. Ma réponse manque peut-être de certaines des raisons pour lesquelles une fermeture est utilisée, mais ce n’est ni de façon flagrante, ni bâclée, ni dangereusement incorrecte. De nombreux articles et discussions sur les clôtures sont centrés sur la programmation fonctionnelle et la notation lambda. Toute ma réponse est que cette explication de la programmation fonctionnelle m'a aidé à comprendre les fermetures. Cela et la valeur persistante.
Bendrix
1
La réponse n’est peut- être pas dangereuse , mais elle est certainement "clairement […] incorrecte". Il utilise les mauvais termes pour des concepts hautement complémentaires et donc souvent utilisés ensemble - exactement là où la clarté de la distinction est essentielle. Cela risque de poser des problèmes de conception aux programmeurs qui le liront s'ils ne sont pas traités. (Et puisque, autant que je sache, l'exemple de code ne contient simplement aucune fermeture, mais seulement une fonction lambda, cela n'aide pas à expliquer pourquoi les fermetures seraient utiles.)
Nathan Tuggy Le
Nathan, cet exemple de code est une fermeture à Swift. Vous pouvez demander à quelqu'un d'autre si vous doutez de moi. Donc, si c'est une fermeture, voudriez-vous la voter?
Bendrix
1
Apple décrit assez clairement ce qu’ils veulent dire exactement par fermeture dans leur documentation . "Les fermetures sont des blocs de fonctionnalités autonomes qui peuvent être échangés et utilisés dans votre code. Les fermetures dans Swift sont similaires aux blocs en C et Objective-C et aux lambdas dans d'autres langages de programmation." Lorsque vous arrivez à la mise en œuvre et à la terminologie, cela peut être déroutant .