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? mieux servi par une fermeture?
Des exemples de fermetures dans Swift sont des accès à une NSUrl et utilisent le géocodeur inversé. Voici un tel exemple. Malheureusement, ces cours ne font que présenter la fermeture; ils ne le font pas expliquer pourquoi la solution de code est écrite comme une fermeture.
Un exemple d'un problème de programmation du monde réel qui pourrait amener mon cerveau à dire, "aha, je devrais écrire une fermeture pour cela", serait plus informatif qu'une discussion théorique. Les discussions théoriques ne manquent pas sur ce site.
Tout d'abord, rien n'est impossible sans utiliser 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 meilleure pratique.
Lorsque les fermetures brillent vraiment sur les autres constructions, c'est lorsque vous utilisez des fonctions d'ordre supérieur, lorsque vous avez réellement besoin de communiquer l'état, et vous pouvez en faire une ligne unique , 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, threshold
est très succinctement et naturellement communiqué de l'endroit où il est défini à l'endroit où il est utilisé. Son champ d'application est précisément limité le plus petit possible. filter
n'a pas besoin d'être écrit pour permettre la possibilité de passer 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 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. De plus, JavaScript a une syntaxe lambda assez verbeuse. Dans Scala, par exemple, l'ensemble du corps de fonction serait:
bookList filter (_.sales >= threshold)
Si vous pouvez cependant utiliser ECMAScript 6 , grâce aux fonctions flèches grasses 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-partout juste pour communiquer des valeurs temporaires d'un endroit à un autre. Ce sont d'excellentes occasions d'envisager de remplacer par une fermeture.
À titre d'explication, je vais emprunter du code à cet excellent article de blog sur les fermetures . C'est JavaScript, mais c'est le langage que la plupart des articles de blog parlent de fermetures, 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 sur la façon dont chaque élément du tableau sera rendu. Si vous souhaitez contrôler le rendu, vous pouvez le faire:
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é.
Et si vous vouliez 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 de total en cours d'exécution, et vous permet d'écrire un rendu qui peut garder une trace du 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 produit ici est que renderInt
conserve l'accès à la variable total
, même si renderInt
est appelée à plusieurs reprises et se termine.
Dans un langage plus traditionnellement orienté objet que JavaScript, vous pouvez écrire une classe qui contient cette variable totale et la transmettre au lieu de créer une fermeture. Mais une fermeture est une façon beaucoup plus puissante, propre et élégante de le faire.
Le but de closures
est simplement de conserver l'état; d'où le nom closure
- it ferme sur l'état. Pour plus d'explications, je vais utiliser Javascript.
En règle générale, vous avez une fonction
function sayHello(){
var txt="Hello";
return txt;
}
où la portée de la ou des variables est liée à cette fonction. Ainsi, après l'exécution, la variable txt
sort du cadre. Il n'y a aucun moyen d'y accéder ou de l'utiliser une fois la fonction terminée.
Les fermetures sont des constructions de langage qui permettent - comme dit plus haut - 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 actions suivantes: 1
- prend une ou plusieurs fonctions en entrée
- génère une fonction
Un exemple simple mais certes pas trop utile est:
makeadder=function(a){
return function(b){
return a+b;
}
}
add5=makeadder(5);
console.log(add5(10));
Vous définissez une fonction makedadder
, qui prend un paramètre en entrée et renvoie une fonction . Il existe une fonction extérieure function(a){}
et une intérieure function(b){}{}
. De plus, vous définissez (implicitement) une autre fonction add5
comme résultat de l'appel de la fonction d'ordre supérieur makeadder
. makeadder(5)
renvoie une fonction anonyme ( intérieure ), qui à son tour prend 1 paramètre et renvoie la somme du paramètre de la fonction externe et paramètre de la fonction interne .
L'astuce est que, tout en retournant la fonction intérieure , qui fait le en ajoutant, 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 en montrer un exemple au moins 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 ce qu'on appelle IIFE = expression de fonction immédiatement invoquée. Il est très courant en javascript de fausses variables de membres privés. Cela se fait via une fonction, qui crée un privé scope = closure
, car il 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 modèle 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 à leur tour ont accès aux 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 qui serait le mieux servi par une fermeture?
Il y a plusieurs raisons à cela. On pourrait être, qu'il est naturel pour lui, car il suit un paradigme fonctionnel . Ou en Javascript: il est simplement nécessaire de s'appuyer sur des fermetures pour contourner certaines bizarreries du langage.
Il existe deux principaux cas d'utilisation pour les fermetures:
Asynchronie. Supposons que vous souhaitiez effectuer une tâche qui prendra un certain temps, puis faire quelque chose une fois terminée. Vous pouvez soit faire attendre votre code, ce qui bloque toute exécution et peut rendre votre programme insensible, soit appeler votre tâche de manière asynchrone et dire "commencer cette longue tâche en arrière-plan, et lorsqu'elle se termine, exécuter cette fermeture", où la fermeture contient le code à exécuter une fois terminé.
Callbacks. Ceux-ci sont également appelés "délégués" ou "gestionnaires d'événements" selon la langue et 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 met en place. Par exemple, dans l'interface utilisateur de votre programme, vous pouvez avoir un bouton, et vous lui donnez une fermeture qui contient le code à exécuter lorsque l'utilisateur clique sur le bouton.
Il existe plusieurs autres utilisations des fermetures, mais ce sont les deux principales.
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 un tri assez rigide. Une bien meilleure approche consiste à recevoir une fonction de comparaison comme argument de la fonction de tri. Parfois, une fonction de comparaison sans état fonctionne correctement (par exemple, trier une liste de nombres ou de noms), mais qu'en est-il si la comparaison a besoin d'un état?
Par exemple, envisagez de trier une liste de villes par distance jusqu'à 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
La fonction Rand()
d'origine 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 l'état simple mais terrible Rand()
nécessaire. Lisez un article de journal mathématique sur un nouveau générateur de nombres aléatoires et vous verrez inévitablement des variables globales. C'est sympa pour les développeurs de la technique, pas si sympa pour les appelants. Encapsuler cet état dans une structure et passer la structure au générateur de nombres aléatoires est un moyen de contourner 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 de Rand()
et la réentrance de l'état encapsulé.
Les nombres aléatoires sont bien plus qu'un simple PRNG. La plupart des gens qui veulent le hasard veulent qu'il soit distribué d'une certaine manière. Je vais commencer par des nombres tirés au hasard entre 0 et 1, ou U (0,1) pour faire court. Tout PRNG qui génère des entiers 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 l'implémenter est de créer une fermeture qui prend une fermeture (le PRNG) et le maximum comme 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 une certaine moyenne et un écart type. Chaque algorithme de générateur de distribution normal que j'ai rencontré utilise un générateur U (0,1). Une façon 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 comme état. C'est, au moins conceptuellement, une fermeture qui prend une fermeture qui prend une fermeture comme argument.
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 est qu'elles peuvent être utilisées facilement partout où vous attendez une fonction: par exemple des fonctions d'ordre supérieur, des rappels simples (ou un 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 une fermeture ou des objets est surtout une question de style. Voici un exemple de choses que les fermetures facilitent mais sont peu pratiques à implémenter 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 caché accessible uniquement via des fermetures globales: vous n'avez pas besoin de faire référence à un objet, utilisez uniquement le protocole défini par les trois fonctions.
Je fais confiance 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. Dans le cas des langages récupérés, cependant, la durée de vie des objets est généralement illimitée, et il est donc possible de construire une fermeture qui pourrait ê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 gardera 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 désignateur de fonction function
et retournons deux fermetures, les deux capturant une variable locale nommée active
:
function
, uniquement lorsque active
est vraiaction
sur nil
, a.k.a. false
.Au lieu de (when active ...)
, il est bien sûr possible d'avoir un (assert active)
expression, qui pourrait lever une exception au cas où la fermeture est appelée alors qu'elle ne devrait pas l'être. En outre, gardez à l'esprit que le code dangereux peut déjà lever une exception seul 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 variables locales active
ne sont pas partagées entre f
et g
; aussi, en plus de active
, f
fait uniquement référence à obj1
et g
fait uniquement référence à obj2
.
L'autre point mentionné par supercat est que les fermetures peuvent entraîner des fuites de mémoire, mais malheureusement, c'est le cas pour presque tout dans les environnements récupérés. S'ils sont disponibles, cela peut être résolu par pointeurs faibles (la fermeture elle-même peut être conservée en mémoire, mais n'empêche pas la récupération de place d'autres ressources ).
Rien de ce qui n'a pas déjà été dit, mais peut-être un exemple plus simple.
Voici un exemple JavaScript utilisant des délais d'expiration:
// 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 lorsque delayedLog()
est appelée, elle revient immédiatement après avoir défini le délai d'expiration, et le délai d'expiration continue de se dérouler en arrière-plan.
Mais lorsque le délai est écoulé et appelle la fonction fire()
, la console affiche le message
qui a été initialement transmis à delayedLog()
, car il est toujours disponible pour fire()
via la fermeture. Vous pouvez appeler delayedLog()
autant que vous le souhaitez, avec un message et un délai différents à chaque fois, et cela fera la bonne chose.
Mais imaginons que JavaScript ne soit pas fermé.
Une façon serait de rendre setTimeout()
bloquant - plus comme une fonction "sleep" - pour que la portée de delayedLog()
ne disparaisse pas tant que le timeout n'est pas écoulé. Mais tout bloquer n'est pas très sympa.
Une autre façon serait de mettre la variable message
dans une autre portée qui sera accessible après que la portée de delayedLog()
aura disparu.
Vous pouvez utiliser des variables globales - ou du moins "de portée plus large", mais vous devrez trouver comment garder une trace de quel message va avec quel délai. Mais il ne peut pas s'agir uniquement d'une file d'attente séquentielle, FIFO, car vous pouvez définir le délai de votre choix. Il peut donc s'agir de "premier entré, troisième sorti" ou quelque chose du genre. un autre moyen de lier une fonction temporisée aux variables dont elle a besoin.
Vous pouvez instancier un objet timeout qui "regroupe" le temporisateur avec le message. Le contexte d'un objet est plus ou moins une étendue qui reste en place. Ensuite, vous devez exécuter le temporisateur dans le contexte de l'objet, afin qu'il ait accès au bon message. Mais vous devez stocker cet objet car sans références, il serait récupéré (sans fermetures, il n'y aurait pas non plus de références implicites). Et vous devrez retirer l'objet une fois qu'il a expiré, sinon il ne fera que rester. Vous auriez donc besoin d'une sorte de liste d'objets d'expiration, et vérifiez périodiquement les objets "épuisés" à supprimer - ou les objets s'ajouteraient et se retireraient de la liste, et ...
Alors ... ouais, ça devient terne.
Heureusement, vous n'avez pas à utiliser une portée plus large ou à agiter des objets juste 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 variable message
lorsque vous en avez besoin. Et à cause de cela, vous pouvez vous en sortir en écrivant delayedLog()
comme ci-dessus.
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 les utilisateurs/api/ URI . Il s'agit en fait d'une fonction 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 fait.
Le conteneur injection de dépendance est disponible (via la clause use) à l'intérieur de 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 à maintenir.