Article de reference

Clôture (programmation informatique)

Dans les langages de programmation , une fermeture , également fermeture lexicale ou fermeture de fonction , est une technique permettant d'implémenter une liaison de nom à port...

Dans les langages de programmation , une fermeture , également fermeture lexicale ou fermeture de fonction , est une technique permettant d'implémenter une liaison de nom à portée lexicale dans un langage avec des fonctions de première classe . Sur le plan opérationnel , une fermeture est un enregistrement stockant une fonction avec un environnement. L'environnement est un mappage associant chaque variable libre de la fonction (variables utilisées localement, mais définies dans une portée englobante) à la valeur ou à la référence à laquelle le nom était lié lors de la création de la fermeture. Contrairement à une fonction simple, une fermeture permet à la fonction d'accéder à ces variables capturées via les copies de la fermeture de leurs valeurs ou références, même lorsque la fonction est invoquée en dehors de leur portée.

Histoire et étymologie

Le concept de fermetures a été développé dans les années 1960 pour l'évaluation mécanique des expressions dans le λ-calcul et a été entièrement implémenté pour la première fois en 1970 en tant que fonctionnalité de langage dans le langage de programmation PAL pour prendre en charge les fonctions de première classe à portée lexicale .

Peter Landin a défini le terme fermeture en 1964 comme ayant une partie environnement et une partie contrôle comme utilisé par sa machine SECD pour évaluer les expressions. Joel Moses attribue à Landin l'introduction du terme fermeture pour désigner une expression lambda avec des liaisons ouvertes (variables libres) qui ont été fermées par (ou liées dans) l'environnement lexical, résultant en une expression fermée , ou fermeture. Cette utilisation a été ensuite adoptée par Sussman et Steele lorsqu'ils ont défini Scheme en 1975, une variante de Lisp à portée lexicale , et s'est répandue.

Sussman et Abelson utilisent également le terme fermeture dans les années 1980 avec une seconde signification, sans rapport : la propriété d'un opérateur qui ajoute des données à une structure de données de pouvoir également ajouter des structures de données imbriquées. Cette utilisation du terme vient de l'usage mathématique , plutôt que de l'usage antérieur en informatique. Les auteurs considèrent que ce chevauchement terminologique est « regrettable ».

Fonctions anonymes

Le terme fermeture est souvent utilisé comme synonyme de fonction anonyme , bien que strictement, une fonction anonyme soit un littéral de fonction sans nom, tandis qu'une fermeture est une instance d'une fonction, une valeur , dont les variables non locales ont été liées soit à des valeurs, soit à des emplacements de stockage (selon la langue ; voir la section environnement lexical ci-dessous).

Par exemple, dans le code Python suivant :

def 
f ( x ): 
def 
g ( y ): 
return 
x 
+ 
y 
return 
g 
# Renvoie une fermeture.
def 
h ( x ): 
return 
lambda 
y : 
x 
+ 
y 
# Renvoie une fermeture.
# Affectation de fermetures spécifiques à des variables. 
a 
= 
f ( 1 ) 
b 
= 
h ( 1 )
# Utilisation des fermetures stockées dans des variables. 
assert 
a ( 5 ) 
== 
6 
assert 
b ( 5 ) 
== 
6
# Utiliser des fermetures sans les lier d'abord à des variables. 
assert 
f ( 1 )( 5 ) 
== 
6 
# f(1) est la fermeture. 
assert 
h ( 1 )( 5 ) 
== 
6 
# h(1) est la fermeture.

les valeurs de aet bsont des fermetures, dans les deux cas produites en renvoyant une fonction imbriquée avec une variable libre de la fonction englobante, de sorte que la variable libre se lie à la valeur du paramètre xde la fonction englobante. Les fermetures dans aet bsont fonctionnellement identiques. La seule différence d'implémentation est que dans le premier cas, nous avons utilisé une fonction imbriquée avec un nom, g, tandis que dans le second cas, nous avons utilisé une fonction imbriquée anonyme (en utilisant le mot-clé Python lambdapour créer une fonction anonyme). Le nom d'origine, le cas échéant, utilisé pour les définir n'est pas pertinent.

Une fermeture est une valeur comme toute autre valeur. Elle n'a pas besoin d'être affectée à une variable et peut être utilisée directement, comme le montrent les deux dernières lignes de l'exemple. Cette utilisation peut être considérée comme une « fermeture anonyme ».

Les définitions de fonctions imbriquées ne sont pas elles-mêmes des fermetures : elles ont une variable libre qui n'est pas encore liée. Ce n'est qu'une fois que la fonction englobante est évaluée avec une valeur pour le paramètre que la variable libre de la fonction imbriquée est liée, créant une fermeture, qui est ensuite renvoyée par la fonction englobante.

Enfin, une fermeture n'est distincte d'une fonction avec des variables libres que lorsqu'elle se trouve en dehors de la portée des variables non locales, sinon l'environnement de définition et l'environnement d'exécution coïncident et rien ne permet de les distinguer (les liaisons statiques et dynamiques ne peuvent pas être distinguées car les noms se résolvent sur les mêmes valeurs). Par exemple, dans le programme ci-dessous, les fonctions avec une variable libre x(liée à la variable non locale xavec portée globale) sont exécutées dans le même environnement où xest définie, il importe donc peu qu'il s'agisse réellement de fermetures :

x 
= 
1 
nombres 
= 
[ 1 , 
2 , 
3 ]
def 
f ( y ): 
renvoie 
x 
+ 
y
carte ( f , 
nums ) 
carte ( lambda 
y : 
x 
+ 
y , 
nums )

Ceci est le plus souvent réalisé par un retour de fonction, puisque la fonction doit être définie dans la portée des variables non locales, auquel cas sa propre portée sera généralement plus petite.

Cela peut également être réalisé par l'ombrage de variables (qui réduit la portée de la variable non locale), bien que cela soit moins courant dans la pratique, car c'est moins utile et l'ombrage est déconseillé. Dans cet exemple, fon peut voir qu'il s'agit d'une fermeture car xle corps de fest lié à xl'espace de noms global, et non à l' espace de xnoms local g:

x 
= 
0
def 
f ( y ): 
renvoie 
x 
+ 
y
def 
g ( z ): 
x 
= 
1 
# ombres locales x globales 
retour 
f ( z )
g ( 1 ) 
# est évalué à 1, pas à 2

Applications

L'utilisation de fermetures est associée à des langages dans lesquels les fonctions sont des objets de première classe , dans lesquels les fonctions peuvent être renvoyées comme résultats de fonctions d'ordre supérieur , ou transmises comme arguments à d'autres appels de fonctions ; si les fonctions avec des variables libres sont de première classe, le renvoi d'une fonction crée une fermeture. Cela inclut les langages de programmation fonctionnelle tels que Lisp et ML , et de nombreux langages modernes à paradigmes multiples, tels que Julia , Python et Rust . Les fermetures sont également souvent utilisées avec des rappels , en particulier pour les gestionnaires d'événements , comme dans JavaScript , où elles sont utilisées pour les interactions avec une page Web dynamique .

Les fermetures peuvent également être utilisées dans un style de passage de continuation pour masquer l'état . Des constructions telles que des objets et des structures de contrôle peuvent ainsi être implémentées avec des fermetures. Dans certains langages, une fermeture peut se produire lorsqu'une fonction est définie dans une autre fonction et que la fonction interne fait référence à des variables locales de la fonction externe. Au moment de l'exécution , lorsque la fonction externe s'exécute, une fermeture est formée, composée du code de la fonction interne et des références (les upvalues) à toutes les variables de la fonction externe requises par la fermeture.

Des fonctions de premier ordre

Les fermetures apparaissent généralement dans les langages dotés de fonctions de première classe . En d'autres termes, ces langages permettent de transmettre des fonctions en tant qu'arguments, de les renvoyer à partir d'appels de fonctions, de les lier à des noms de variables, etc., tout comme des types plus simples tels que des chaînes et des entiers. Par exemple, considérons la fonction Scheme suivante :

; Renvoie une liste de tous les livres avec au moins THRESHOLD exemplaires vendus. 
( define ( best-selling-books threshold ) ( filter ( lambda ( book ) ( >= ( book-sales book ) threshold )) book-list ))

Dans cet exemple, l' expression lambda (lambda (book) (>= (book-sales book) threshold)) apparaît dans la fonction best-selling-books. Lorsque l'expression lambda est évaluée, Scheme crée une fermeture composée du code de l'expression lambda et d'une référence à la thresholdvariable, qui est une variable libre à l'intérieur de l'expression lambda.

La fermeture est ensuite transmise à la filterfonction, qui l'appelle à plusieurs reprises pour déterminer quels livres doivent être ajoutés à la liste de résultats et lesquels doivent être supprimés. Étant donné que la fermeture a une référence à threshold, elle peut utiliser cette variable à chaque fois filterqu'elle l'appelle. La fonction filterpeut être définie dans un fichier séparé.

Voici le même exemple réécrit en JavaScript , un autre langage populaire avec support des fermetures :

// Renvoie une liste de tous les livres avec au moins 'threshold' copies vendues. function bestSellingBooks ( threshold ) { return bookList.filter ( book => book.sales > = threshold ) ; }

L'opérateur flèche =>est utilisé pour définir une expression de fonction flèche et une Array.filterméthode au lieu d'une filterfonction globale, mais sinon la structure et l'effet du code sont les mêmes.

Une fonction peut créer une fermeture et la renvoyer, comme dans cet exemple :

// Renvoie une fonction qui approxime la dérivée de f // en utilisant un intervalle de dx, qui doit être suffisamment petit. 
function derived ( f , dx ) { return x => ( f ( x + dx ) -f ( x )) / dx ; }

Étant donné que la fermeture dans ce cas survit à l'exécution de la fonction qui la crée, les variables fet dxsurvivent après derivativele retour de la fonction, même si l'exécution a quitté leur portée et qu'elles ne sont plus visibles. Dans les langages sans fermetures, la durée de vie d'une variable locale automatique coïncide avec l'exécution de la pile dans laquelle cette variable est déclarée. Dans les langages avec fermetures, les variables doivent continuer d'exister tant que des fermetures existantes ont des références à elles. Ceci est le plus souvent implémenté à l'aide d'une forme de garbage collection .

Représentation de l'État

Une fermeture peut être utilisée pour associer une fonction à un ensemble de variables « privées », qui persistent après plusieurs invocations de la fonction. La portée de la variable englobe uniquement la fonction fermée, elle n'est donc pas accessible à partir d'un autre code de programme. Celles-ci sont analogues aux variables privées dans la programmation orientée objet , et en fait les fermetures sont similaires aux objets de fonction à état (ou foncteurs) avec une seule méthode d'appel-opérateur.

Dans les langages à états, les fermetures peuvent ainsi être utilisées pour implémenter des paradigmes de représentation d'état et de masquage d'informations , puisque les upvalues ​​de la fermeture (ses variables fermées) ont une étendue indéfinie , de sorte qu'une valeur établie dans une invocation reste disponible dans la suivante. Les fermetures utilisées de cette manière n'ont plus de transparence référentielle , et ne sont donc plus des fonctions pures ; néanmoins, elles sont couramment utilisées dans des langages fonctionnels impurs tels que Scheme .

Autres utilisations

Les fermetures ont de nombreuses utilisations :

  • Comme les fermetures retardent l'évaluation (c'est-à-dire qu'elles ne font rien tant qu'elles ne sont pas appelées), elles peuvent être utilisées pour définir des structures de contrôle. Par exemple, toutes les structures de contrôle standard de Smalltalk , y compris les branches (if/then/else) et les boucles (while et for), sont définies à l'aide d'objets dont les méthodes acceptent les fermetures. Les utilisateurs peuvent également définir facilement leurs propres structures de contrôle.
  • Dans les langages qui implémentent l'affectation, plusieurs fonctions peuvent être produites qui se ferment sur le même environnement, leur permettant de communiquer de manière privée en modifiant cet environnement. Dans Scheme :
( définir foo #f ) ( définir bar #f )
( let (( secret-message "none" )) ( set! foo ( lambda ( msg ) ( set! secret-message msg ))) ( set! bar ( lambda () secret-message )))
( display ( bar )) ; imprime "none" ( newline ) ( foo " retrouvez-moi sur les quais à minuit " ) ( display ( bar )) ; imprime " retrouvez-moi sur les quais à minuit "
  • Les fermetures peuvent être utilisées pour implémenter des systèmes d'objets .

Remarque : certains locuteurs appellent fermeture toute structure de données qui lie un environnement lexical , mais le terme fait généralement référence spécifiquement aux fonctions.

Mise en œuvre et théorie

Les fermetures sont généralement implémentées avec une structure de données spéciale qui contient un pointeur vers le code de la fonction , ainsi qu'une représentation de l'environnement lexical de la fonction (c'est-à-dire l'ensemble des variables disponibles) au moment de la création de la fermeture. L'environnement de référence lie les noms non locaux aux variables correspondantes dans l'environnement lexical au moment de la création de la fermeture, prolongeant en outre leur durée de vie au moins aussi longtemps que la durée de vie de la fermeture. Lorsque la fermeture est saisie ultérieurement, éventuellement avec un environnement lexical différent, la fonction est exécutée avec ses variables non locales faisant référence à celles capturées par la fermeture, et non à l'environnement actuel.

Une implémentation de langage ne peut pas facilement prendre en charge les fermetures complètes si son modèle de mémoire d'exécution alloue toutes les variables automatiques sur une pile linéaire . Dans de tels langages, les variables locales automatiques d'une fonction sont désallouées lorsque la fonction revient. Cependant, une fermeture nécessite que les variables libres auxquelles elle fait référence survivent à l'exécution de la fonction englobante. Par conséquent, ces variables doivent être allouées de manière à persister jusqu'à ce qu'elles ne soient plus nécessaires, généralement via l'allocation de tas , plutôt que sur la pile, et leur durée de vie doit être gérée de manière à ce qu'elles survivent jusqu'à ce que toutes les fermetures qui les référencent ne soient plus utilisées.

Cela explique pourquoi, généralement, les langages qui supportent nativement les fermetures utilisent également le garbage collection . Les alternatives sont la gestion manuelle de la mémoire des variables non locales (allocation explicite sur le tas et libération une fois terminé), ou, si l'on utilise l'allocation de pile, que le langage accepte que certains cas d'utilisation conduisent à un comportement indéfini , en raison de pointeurs suspendus vers des variables automatiques libérées, comme dans les expressions lambda en C++11 ou les fonctions imbriquées dans GNU C. Le problème funarg (ou problème de « l'argument fonctionnel ») décrit la difficulté d'implémenter des fonctions en tant qu'objets de première classe dans un langage de programmation basé sur la pile tel que C ou C++. De même, dans la version 1 de D , on suppose que le programmeur sait quoi faire avec les délégués et les variables locales automatiques, car leurs références seront invalides après le retour de sa portée de définition (les variables locales automatiques sont sur la pile) - cela permet toujours de nombreux modèles fonctionnels utiles, mais pour les cas complexes, il faut une allocation explicite du tas pour les variables. La version 2 de D a résolu ce problème en détectant les variables qui doivent être stockées sur le tas et en effectuant une allocation automatique. Étant donné que D utilise le ramasse-miettes, dans les deux versions, il n'est pas nécessaire de suivre l'utilisation des variables au fur et à mesure de leur transmission.

Dans les langages strictement fonctionnels avec des données immuables ( par exemple Erlang ), il est très facile d'implémenter une gestion automatique de la mémoire (garbage collection), car il n'y a pas de cycles possibles dans les références des variables. Par exemple, dans Erlang, tous les arguments et variables sont alloués sur le tas, mais les références à ceux-ci sont en plus stockées sur la pile. Après le retour d'une fonction, les références sont toujours valides. Le nettoyage du tas est effectué par un garbage collector incrémental.

Dans ML, les variables locales ont une portée lexicale et définissent donc un modèle de type pile, mais comme elles sont liées à des valeurs et non à des objets, une implémentation est libre de copier ces valeurs dans la structure de données de la fermeture d'une manière invisible pour le programmeur.

Scheme , qui possède un système de portée lexicale de type ALGOL avec des variables dynamiques et un ramasse-miettes, ne dispose pas d'un modèle de programmation par pile et ne souffre pas des limitations des langages basés sur la pile. Les fermetures sont exprimées naturellement dans Scheme. La forme lambda enferme le code, et les variables libres de son environnement persistent dans le programme aussi longtemps qu'elles sont accessibles, et peuvent donc être utilisées aussi librement que n'importe quelle autre expression Scheme.

Les fermetures sont étroitement liées aux acteurs dans le modèle d'acteurs de calcul concurrent où les valeurs de l'environnement lexical de la fonction sont appelées connaissances . Un problème important pour les fermetures dans les langages de programmation concurrents est de savoir si les variables d'une fermeture peuvent être mises à jour et, si oui, comment ces mises à jour peuvent être synchronisées. Les acteurs fournissent une solution.

Les fermetures sont étroitement liées aux objets fonctionnels ; la transformation des premières en secondes est connue sous le nom de défonctionnalisation ou lambda lifting ; voir aussi conversion de fermeture .

Différences de sémantique

Environnement lexical

Comme les différents langages n'ont pas toujours une définition commune de l'environnement lexical, leurs définitions de la fermeture peuvent également varier. La définition minimaliste communément admise de l'environnement lexical le définit comme un ensemble de toutes les liaisons de variables dans la portée, et c'est également ce que les fermetures dans n'importe quel langage doivent capturer. Cependant, la signification d'une liaison de variable diffère également. Dans les langages impératifs, les variables se lient à des emplacements relatifs en mémoire qui peuvent stocker des valeurs. Bien que l'emplacement relatif d'une liaison ne change pas au moment de l'exécution, la valeur dans l'emplacement lié peut le faire. Dans ces langages, puisque la fermeture capture la liaison, toute opération sur la variable, qu'elle soit effectuée à partir de la fermeture ou non, est effectuée sur le même emplacement mémoire relatif. C'est ce qu'on appelle souvent la capture de la variable « par référence ». Voici un exemple illustrant le concept dans ECMAScript , qui est l'un de ces langages :

// Javascript 
var f , g ; function foo () { var x ; f = function () { return ++ x ; }; g = function () { return -- x ; }; x = 1 ; alert ( 'à l'intérieur de foo, appel à f(): ' + f ()); } foo (); // 2 alert ( 'appel à g(): ' + g ()); // 1 (--x) alert ( 'appel à g(): ' + g ()); // 0 (--x) alert ( 'appel à f(): ' + f ()); // 1 (++x) alert ( 'appel à f(): ' + f ()); // 2 (++x)

La fonction fooet les fermetures référencées par les variables futilisent gtoutes le même emplacement de mémoire relatif signifié par la variable locale x.

Dans certains cas, le comportement ci-dessus peut être indésirable et il est alors nécessaire de lier une fermeture lexicale différente. Encore une fois, dans ECMAScript, cela se ferait en utilisant le Function.bind().

Exemple 1 : Référence à une variable non liée

var module = { x : 42 , getX : function () { return this . x ; } } var unboundGetX = module . getX ; console . log ( unboundGetX ()); // La fonction est invoquée au niveau de la portée globale // émet undefined car 'x' n'est pas spécifié dans la portée globale.
var boundGetX = unboundGetX . bind ( module ); // spécifie l'objet module comme fermeture console . log ( boundGetX ()); // émet 42

Exemple 2 : Référence accidentelle à une variable liée

Pour cet exemple, le comportement attendu serait que chaque lien émette son identifiant lorsqu'il est cliqué ; mais comme la variable « e » est liée à la portée ci-dessus et évaluée paresseusement lors du clic, ce qui se passe réellement est que chaque événement de clic émet l'identifiant du dernier élément de « éléments » lié à la fin de la boucle for.

var elements = document . getElementsByTagName ( 'a' ); // Incorrect : e est lié à la fonction contenant la boucle 'for', pas à la fermeture de "handle" for ( var e of elements ) { e . onclick = function handle () { alert ( e . id ); } }

Ici encore, la variable edevrait être liée à la portée du bloc en utilisant handle.bind(this)ou le letmot-clé.

D'un autre côté, de nombreux langages fonctionnels, tels que ML , lient directement les variables aux valeurs. Dans ce cas, comme il n'y a aucun moyen de modifier la valeur de la variable une fois qu'elle est liée, il n'est pas nécessaire de partager l'état entre les fermetures : elles utilisent simplement les mêmes valeurs. C'est ce qu'on appelle souvent la capture de la variable « par valeur ». Les classes locales et anonymes de Java entrent également dans cette catégorie : elles nécessitent que les variables locales capturées soient final, ce qui signifie également qu'il n'est pas nécessaire de partager l'état.

Certains langages permettent de choisir entre capturer la valeur d'une variable ou son emplacement. Par exemple, en C++11, les variables capturées sont soit déclarées avec [&], ce qui signifie capturées par référence, soit avec [=], ce qui signifie capturées par valeur.

Un autre sous-ensemble, les langages fonctionnels paresseux tels que Haskell , lient les variables aux résultats de calculs futurs plutôt qu'aux valeurs. Considérez cet exemple en Haskell :

-- Haskell 
foo :: Fractional a => a -> a -> ( a -> a ) foo x y = ( \ z -> z + r ) r = x / y
f :: Fractionnaire a => a -> a f = foo 1 0
main = imprimer ( f 123 )

La liaison rcapturée par la fermeture définie dans la fonction fooest liée au calcul (x / y), ce qui dans ce cas aboutit à une division par zéro. Cependant, comme c'est le calcul qui est capturé, et non la valeur, l'erreur ne se manifeste que lorsque la fermeture est invoquée, puis tente d'utiliser la liaison capturée.

Fermeture départ

D'autres différences se manifestent dans le comportement d'autres constructions à portée lexicale, telles que les instructions return, breaket continue. De telles constructions peuvent, en général, être considérées en termes d'invocation d'une continuation d'échappement établie par une instruction de contrôle englobante (dans le cas de breaket continue, une telle interprétation nécessite que les constructions de boucle soient considérées en termes d'appels de fonctions récursives). Dans certains langages, tels que ECMAScript, returnfait référence à la continuation établie par la fermeture lexicalement la plus interne par rapport à l'instruction. Ainsi, a returndans une fermeture transfère le contrôle au code qui l'a appelé. Cependant, dans Smalltalk , l'opérateur superficiellement similaire ^invoque la continuation d'échappement établie pour l'invocation de la méthode, en ignorant les continuations d'échappement de toutes les fermetures imbriquées intermédiaires. La continuation d'échappement d'une fermeture particulière ne peut être invoquée implicitement dans Smalltalk qu'en atteignant la fin du code de la fermeture. Ces exemples dans ECMAScript et Smalltalk mettent en évidence la différence :

"Smalltalk" 
foo 
| xs | 
xs 
:= 
#( 1 
2 
3 
4 ) . 
xs 
do: [ : x 
| 
^ x ] . 
^ 0 
bar 
Transcription 
afficher: ( self 
foo 
printString ) "imprime 1"
// Fonction ECMAScript foo () { var xs = [ 1 , 2 , 3 , 4 ]; xs . forEach ( function ( x ) { return x ; }); return 0 ; } alert ( foo ()); // affiche 0

Les extraits de code ci-dessus se comporteront différemment car l' ^opérateur Smalltalk et l'opérateur JavaScript returnne sont pas analogues. Dans l'exemple ECMAScript, return xla fermeture interne sera abandonnée pour commencer une nouvelle itération de la forEachboucle, tandis que dans l'exemple Smalltalk, ^xla boucle sera abandonnée et la méthode reviendra foo.

Common Lisp fournit une construction qui peut exprimer l'une des actions ci-dessus : Lisp (return-from foo x)se comporte comme Smalltalk ^x , tandis que Lisp (return-from nil x)se comporte comme JavaScript return x . Par conséquent, Smalltalk permet à une continuation d'échappement capturée de survivre au-delà de la mesure dans laquelle elle peut être invoquée avec succès. Considérez :

"Smalltalk" 
foo 
^ [ : x 
| 
^ x ] bar 
| 
f 
| 
f 
:= 
self 
foo . 
f 
valeur : 
123 
"erreur !"

Lorsque la fermeture renvoyée par la méthode fooest invoquée, elle tente de renvoyer une valeur à partir de l'invocation de foocelle qui a créé la fermeture. Étant donné que cet appel a déjà été renvoyé et que le modèle d'invocation de méthode Smalltalk ne suit pas la discipline de la pile spaghetti pour faciliter les renvois multiples, cette opération génère une erreur.

Certains langages, comme Ruby , permettent au programmeur de choisir la manière dont le code returnest capturé. Un exemple en Ruby :

#Rubis
# Fermeture à l'aide d'une procédure 
def foo f = Proc . new { return "retour de foo depuis l'intérieur de proc" } f . call # le contrôle quitte foo ici return "retour de foo" end
# Fermeture utilisant un lambda 
def bar f = lambda { return "return from lambda" } f . call # le contrôle ne quitte pas bar ici return "return from bar" end
met foo # imprime "retour de foo depuis l'intérieur de la procédure" met bar # imprime "retour de bar"

Dans cet exemple, les deux méthodes Proc.newet lambdasont des moyens de créer une fermeture, mais la sémantique des fermetures ainsi créées est différente par rapport à l' returninstruction.

Dans Scheme , la définition et la portée de l' returninstruction de contrôle sont explicites (et nommées arbitrairement « return » pour les besoins de l'exemple). Ce qui suit est une traduction directe de l'exemple Ruby.

; Schéma 
( définir l'appel/cc appel-avec-continuation-actuelle )
( define ( foo ) ( call/cc ( lambda ( return ) ( define ( f ) ( return "retour de foo depuis l'intérieur de proc" )) ( f ) ; le contrôle laisse foo ici ( return "retour de foo" ))))
( define ( bar ) ( call/cc ( lambda ( return ) ( define ( f ) ( call/cc ( lambda ( return ) ( return "retour de lambda" )))) ( f ) ; le contrôle ne quitte pas la barre ici ( return "retour de la barre" ))))
( display ( foo )) ; imprime "retour de foo depuis l'intérieur de proc" ( newline ) ( display ( bar )) ; imprime "retour de bar"

Constructions de type fermeture

Certains langages disposent de fonctionnalités qui simulent le comportement des fermetures. Dans des langages tels que C++ , C# , D , Java , Objective-C et Visual Basic (.NET) (VB.NET), ces fonctionnalités sont le résultat du paradigme orienté objet du langage.

Rappels (C)

Certaines bibliothèques C prennent en charge les rappels . Cela est parfois implémenté en fournissant deux valeurs lors de l'enregistrement du rappel auprès de la bibliothèque : un pointeur de fonction et un void*pointeur distinct vers des données arbitraires choisies par l'utilisateur. Lorsque la bibliothèque exécute la fonction de rappel, elle transmet le pointeur de données. Cela permet au rappel de conserver l'état et de faire référence aux informations capturées au moment de son enregistrement auprès de la bibliothèque. L'idiome est similaire aux fermetures en termes de fonctionnalité, mais pas de syntaxe. Le void*pointeur n'est pas de type sécurisé , donc cet idiome C diffère des fermetures de type sécurisé en C#, Haskell ou ML.

Les rappels sont largement utilisés dans les boîtes à outils de widgets d'interface utilisateur graphique (GUI) pour implémenter une programmation pilotée par événements en associant des fonctions générales de widgets graphiques (menus, boutons, cases à cocher, curseurs, spinners, etc.) à des fonctions spécifiques à l'application implémentant le comportement souhaité spécifique pour l'application.

Fonction imbriquée et pointeur de fonction (C)

Avec une extension GNU Compiler Collection (GCC), une fonction imbriquée peut être utilisée et un pointeur de fonction peut émuler des fermetures, à condition que la fonction ne sorte pas de la portée contenante. L'exemple suivant n'est pas valide car adderil s'agit d'une définition de niveau supérieur (selon la version du compilateur, elle pourrait produire un résultat correct si elle était compilée sans optimisation, c'est-à-dire à -O0) :

#include <stdio.h>
typedef int ( * fn_int_to_int )( int ); // type de fonction int->int
fn_int_to_int adder ( int number ) { int add ( int value ) { return value + number ; } return & add ; // L'opérateur & est facultatif ici car le nom d'une fonction en C est un pointeur pointant sur lui-même }
int main ( void ) { fn_int_to_int add10 = adder ( 10 ); printf ( "%d " , add10 ( 1 )); return 0 ; }

Mais le déplacement adder(et, éventuellement, le typedef) dans mainle rend valide :

#include <stdio.h>
int main ( void ) { typedef int ( * fn_int_to_int )( int ); // type de fonction int->int fn_int_to_int adder ( intnumber ) { int add ( intvalue ) { valeur de retour + nombre ; } return add ; } fn_int_to_int add10 = adder ( 10 ) ; printf ( "%d " , add10 ( 1 )); return 0 ; }

Si cela est exécuté, cela s'imprime désormais 11comme prévu.

Classes locales et fonctions lambda (Java)

Java permet de définir des classes à l'intérieur de méthodes . On les appelle classes locales . Lorsque ces classes ne sont pas nommées, on les appelle classes anonymes (ou classes internes anonymes ). Une classe locale (nommée ou anonyme) peut faire référence à des noms dans des classes englobantes lexicalement ou à des variables en lecture seule (marquées comme final) dans la méthode englobante lexicalement.

class 
CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // L'expression "new Runnable() { ... }" est une classe anonyme implémentant l'interface 'Runnable'. new Thread ( new Runnable () { void run () { // Il peut lire les variables locales finales : calculate ( uri ); // Il peut accéder aux champs privés de la classe englobante : result = result + 10 ; } } ). start (); } }

La capture de finalvariables permet de capturer des variables par valeur. Même si la variable à capturer n'est pas de type final, elle peut toujours être copiée dans une finalvariable temporaire juste avant la classe.

La capture de variables par référence peut être émulée en utilisant une finalréférence à un conteneur mutable, par exemple un tableau à un élément. La classe locale ne pourra pas modifier la valeur de la référence du conteneur, mais elle pourra modifier le contenu du conteneur.

Avec l'avènement des expressions lambda de Java 8, la fermeture provoque l'exécution du code ci-dessus comme :

class 
CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // Le code () -> { /* code */ } est une fermeture. new Thread (() -> { calculate ( uri ); result = result + 10 ; }). start (); } }

Les classes locales sont l'un des types de classes internes déclarées dans le corps d'une méthode. Java prend également en charge les classes internes déclarées comme membres non statiques d'une classe englobante. Elles sont normalement appelées simplement « classes internes ». Elles sont définies dans le corps de la classe englobante et ont un accès complet aux variables d'instance de la classe englobante. En raison de leur liaison à ces variables d'instance, une classe interne ne peut être instanciée qu'avec une liaison explicite à une instance de la classe englobante à l'aide d'une syntaxe spéciale.

public class EnclosingClass { /* Définir la classe interne */ public class InnerClass { public int incrementAndReturnCounter () { return counter ++ ; } }
compteur int privé ; { compteur = 0 ; }
public int getCounter () { renvoie un compteur ; }
public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); /* Instanciez la classe interne, avec liaison à l'instance */ EnclosingClass . InnerClass innerClassInstance = enclosingClassInstance . new InnerClass ();
for ( int i = enclosingClassInstance . getCounter (); ( i = innerClassInstance . incrementAndReturnCounter ()) < 10 ; /* étape d'incrémentation omise */ ) { System . out . println ( i ); } } }

Lors de l'exécution, cela imprimera les entiers de 0 à 9. Attention à ne pas confondre ce type de classe avec la classe imbriquée, qui est déclarée de la même manière avec une utilisation accompagnée du modificateur "static" ; celles-ci n'ont pas l'effet désiré mais sont simplement des classes sans liaison spéciale définie dans une classe englobante.

Depuis Java 8 , Java prend en charge les fonctions en tant qu'objets de première classe. Les expressions lambda de cette forme sont considérées comme de type Function<T,U>T, le domaine et U le type d'image. L'expression peut être appelée avec sa .apply(T t)méthode, mais pas avec un appel de méthode standard.

public static void main ( String [] args ) { Fonction < Chaîne , Entier > longueur = s -> s . length ();
System . out . println ( length . apply ( "Bonjour tout le monde !" ) ); // Imprimera 13. }

Blocs (C, C++, Objective-C 2.0)

Apple a introduit les blocs , une forme de fermeture, comme extension non standard dans C , C++ , Objective-C 2.0 et dans Mac OS X 10.6 « Snow Leopard » et iOS 4.0 . Apple a rendu leur implémentation disponible pour les compilateurs GCC et clang.

Les pointeurs vers des blocs et les littéraux de blocs sont marqués avec ^. Les variables locales normales sont capturées par valeur lorsque le bloc est créé et sont en lecture seule à l'intérieur du bloc. Les variables à capturer par référence sont marquées avec __block. Les blocs qui doivent persister en dehors de la portée dans laquelle ils sont créés peuvent avoir besoin d'être copiés.

typedef int ( ^ IntBlock )();
IntBlock downCounter ( int start ) { __block int i = start ; retourner [[ ^ int () { retourner i -- ; } copie ] libération automatique ]; }
IntBlock f = downCounter ( 5 ); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ());

Délégués (C#, VB.NET, D)

Les méthodes anonymes C# et les expressions lambda prennent en charge la fermeture :

var données = nouvelle [] { 1 , 2 , 3 , 4 }; var multiplicateur = 2 ; var résultat = données . Select ( x => x * multiplicateur );

Visual Basic .NET , qui possède de nombreuses fonctionnalités de langage similaires à celles de C#, prend également en charge les expressions lambda avec des fermetures :

Données Dim = { 1 , 2 , 3 , 4 } Multiplicateur Dim = 2 Résultat Dim = données . Sélectionnez ( Fonction ( x ) x * multiplicateur )

En D , les fermetures sont implémentées par des délégués, un pointeur de fonction associé à un pointeur de contexte (par exemple une instance de classe ou un cadre de pile sur le tas dans le cas des fermetures).

auto test1 () { int a = 7 ; return delegate () { return a + 3 ; }; // construction de délégué anonyme }
auto test2 () { int a = 20 ; int foo () { return a + 5 ; } // fonction interne return & foo ; // autre façon de construire un délégué }
void bar () { auto dg = test1 (); dg (); // =10 // ok, test1.a est dans une fermeture et existe toujours
dg = test2 (); dg (); // =25 // ok, test2.a est dans une fermeture et existe toujours }

La version 1 de D a un support de fermeture limité. Par exemple, le code ci-dessus ne fonctionnera pas correctement, car la variable a est sur la pile, et après le retour de test(), il n'est plus valide de l'utiliser (très probablement, l'appel de foo via dg() renverra un entier « aléatoire »). Cela peut être résolu en allouant explicitement la variable « a » sur le tas, ou en utilisant des structures ou une classe pour stocker toutes les variables fermées nécessaires et construire un délégué à partir d'une méthode implémentant le même code. Les fermetures peuvent être passées à d'autres fonctions, à condition qu'elles ne soient utilisées que lorsque les valeurs référencées sont toujours valides (par exemple en appelant une autre fonction avec une fermeture comme paramètre de rappel), et sont utiles pour écrire du code de traitement de données générique, donc cette limitation, en pratique, n'est souvent pas un problème.

Cette limitation a été corrigée dans la version 2 de D : la variable « a » sera automatiquement allouée sur le tas car elle est utilisée dans la fonction interne, et un délégué de cette fonction peut échapper à la portée actuelle (via une affectation à dg ou return). Toutes les autres variables locales (ou arguments) qui ne sont pas référencées par des délégués ou qui ne sont référencées que par des délégués qui n'échappent pas à la portée actuelle, restent sur la pile, ce qui est plus simple et plus rapide que l'allocation de tas. Il en va de même pour les méthodes de classe d'inner qui référencent les variables d'une fonction.

Objets de fonction (C++)

C++ permet de définir des objets de fonction en surchargeant operator(). Ces objets se comportent un peu comme des fonctions dans un langage de programmation fonctionnel. Ils peuvent être créés au moment de l'exécution et peuvent contenir un état, mais ils ne capturent pas implicitement les variables locales comme le font les fermetures. Depuis la révision 2011 , le langage C++ prend également en charge les fermetures, qui sont un type d'objet de fonction construit automatiquement à partir d'une construction de langage spéciale appelée lambda-expression . Une fermeture C++ peut capturer son contexte soit en stockant des copies des variables accédées en tant que membres de l'objet de fermeture, soit par référence. Dans ce dernier cas, si l'objet de fermeture échappe à la portée d'un objet référencé, l'invocation de son operator()comportement provoque un comportement indéfini puisque les fermetures C++ ne prolongent pas la durée de vie de leur contexte.

void foo ( string myname ) { int y ; vector < string > n ; // ... auto i = std :: find_if ( n . begin (), n . end (), // voici l'expression lambda : [ & ]( const string & s ) { return s != myname && s . size () > y ; } ); // 'i' est maintenant soit 'n.end()' soit pointe vers la première chaîne de 'n' // qui n'est pas égale à 'myname' et dont la longueur est supérieure à 'y' }

Agents en ligne (Eiffel)

Eiffel inclut des agents en ligne définissant des fermetures. Un agent en ligne est un objet représentant une routine, défini en donnant le code de la routine en ligne. Par exemple, dans

ok_button . click_event . subscribe ( agent ( x , y : INTEGER ) faire map . country_at_coordinates ( x , y ). display fin )

l'argument to subscribeest un agent, représentant une procédure avec deux arguments ; la procédure trouve le pays aux coordonnées correspondantes et l'affiche. L'agent entier est « abonné » au type d'événement click_eventpour un certain bouton, de sorte que chaque fois qu'une instance du type d'événement se produit sur ce bouton – parce qu'un utilisateur a cliqué sur le bouton – la procédure sera exécutée avec les coordonnées de la souris passées comme arguments pour xet y.

La principale limitation des agents Eiffel, qui les distingue des fermetures dans d'autres langages, est qu'ils ne peuvent pas référencer de variables locales à partir de la portée englobante. Cette décision de conception permet d'éviter toute ambiguïté lorsqu'on parle d'une valeur de variable locale dans une fermeture : doit-il s'agir de la dernière valeur de la variable ou de la valeur capturée lors de la création de l'agent ? Seuls Current(une référence à l'objet actuel, analogue à thisJava), ses fonctionnalités et les arguments de l'agent sont accessibles depuis le corps de l'agent. Les valeurs des variables locales externes peuvent être transmises en fournissant des opérandes fermés supplémentaires à l'agent.

Mot réservé __closure de C++Builder

Embarcadero C++Builder fournit le mot réservé __closurepour fournir un pointeur vers une méthode avec une syntaxe similaire à un pointeur de fonction.

La norme C permet d'écrire un typedef pour un pointeur vers un type de fonction en utilisant la syntaxe suivante :

typedef void ( * TMyFunctionPointer )( void );

De la même manière, un typedef peut être déclaré pour un pointeur vers une méthode en utilisant cette syntaxe :

typedef void ( __closure * TMyMethodPointer )();

Plus d articles de Worldlex Wiki

Revenez a l index pour explorer davantage de pages sur l histoire, la science, la culture, la geographie et la societe en francais.

Explorer l index