Fermeture (programmation informatique)

0

Dans les langages de programmation , une fermeture , également fermeture lexicale ou fermeture de fonction , est une technique pour implémenter une liaison de noms à 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 [a] avec un environnement. [1] L’environnement est un mapping associant chaque Variable libre de la fonction (variables utilisées localement, mais définies dans un périmètre englobant) à la valeur ou référenceauquel le nom était lié lors de la création de la fermeture. [b] 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 mis en œuvre 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 . [2]

Peter J. Landin a défini le terme fermeture en 1964 comme ayant une partie environnement et une partie contrôle telle qu’utilisée par sa machine SECD pour évaluer les expressions. [3] Joel Moses attribue à Landin l’introduction du terme fermeture pour désigner une expression lambda dont les liaisons ouvertes (variables libres) ont été fermées par (ou liées dans) l’environnement lexical, résultant en une expression fermée , ou fermeture. [4] [5] Cet usage a ensuite été adopté par Sussman et Steele lorsqu’ils ont défini Scheme en 1975,[6] une variante à portée lexicale de Lisp , et s’est généralisée.

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 pour pouvoir également ajouter des structures de données imbriquées. Cette utilisation du terme provient de l’utilisation des mathématiques plutôt que de l’utilisation antérieure en informatique. Les auteurs considèrent que ce chevauchement de terminologie est “malheureux”. [7]

Fonctions anonymes

Le terme fermeture est souvent utilisé comme synonyme de fonction anonyme , bien que strictement, une fonction anonyme est une fonction littérale sans nom, tandis qu’une fermeture est une instance d’une fonction, une valeur , dont les variables non locales ont été liées soit à valeurs ou à des emplacements de stockage (selon la langue ; voir la section sur l’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 aux variables. une = f ( 1 ) b = h ( 1 ) # Utilisation des fermetures stockées dans les variables. affirmer un ( 5 ) == 6 affirmer b ( 5 ) == 6 # Utiliser des fermetures sans les lier d’abord aux 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, gtandis 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 n’importe quelle autre valeur. Il n’a pas besoin d’être affecté à une variable et peut être utilisé directement, comme indiqué dans 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 à variables libres que lorsqu’elle est en dehors du périmètre des variables non locales, sinon l’environnement de définition et l’environnement d’exécution coïncident et il n’y a rien pour les distinguer (les liaisons statique et dynamique ne peuvent pas être distinguées car les noms se résolvent aux mêmes valeurs). Par exemple, dans le programme ci-dessous, les fonctions avec une Variable libre x(liée à la variable non locale xavec une portée globale) sont exécutées dans le même environnement où xest défini, il est donc sans importance qu’il s’agisse réellement de fermetures :

x = 1 nombres = [ 1 , 2 , 3 ] def f ( y ): renvoie x + y map ( f , nums ) map ( 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’ occultation variable (qui réduit la portée de la variable non locale), bien que cela soit moins courant dans la pratique, car il est moins utile et l’occultation est déconseillée. Dans cet exemple f, on peut voir qu’il s’agit d’une fermeture car xdans le corps de fest lié à xdans l’espace de noms global, et non xà local g:

x = 0 def f ( y ): renvoie x + y def g ( z ): x = 1 # local x ombres global x return f ( z ) g ( 1 ) # évalue à 1, pas 2

Applications

L’utilisation des fermetures est associée aux langages où les fonctions sont des objets de première classe , dans lesquels les fonctions peuvent être renvoyées en tant que résultats de fonctions d’ordre supérieur , ou transmises en tant qu’arguments à d’autres appels de fonction ; si les fonctions avec des variables libres sont de première classe, alors en renvoyer une crée une fermeture. Cela inclut les Langages de programmation fonctionnels tels que Lisp et ML , ainsi que de nombreux langages modernes multi-paradigmes, tels que Julia , Python et Rust . Les fermetures sont également fréquemment utilisées avec les rappels , en particulier pour les gestionnaires d’événements , comme en JavaScript ., où ils sont utilisés 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 aux 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 valeurs amont) à toutes les variables de la fonction externe requises par la fermeture.

Fonctions de première classe

Les fermetures apparaissent généralement dans les langages avec des fonctions de première classe – en d’autres termes, ces langages permettent aux fonctions d’être passées en tant qu’arguments, renvoyées par des appels de fonction, liées à des noms de variables, etc., tout comme des types plus simples tels que des chaînes et des entiers. Par exemple, considérez la fonction Scheme suivante :

; Renvoie une liste de tous les livres avec au moins SEUIL d’exemplaires vendus. ( définir ( seuil des meilleures ventes de livres ) ( filtre ( lambda ( livre ) ( >= ( livre-ventes livre ) seuil )) liste de livres ))

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. Parce que la fermeture elle-même a une référence à threshold, elle peut utiliser cette variable à chaque fois filterqu’elle l’appelle. La fonction filterelle-même peut être définie dans un fichier complètement séparé.

Voici le même exemple réécrit en JavaScript , un autre langage populaire prenant en charge les fermetures :

// Renvoie une liste de tous les livres avec au moins ‘seuil’ d’exemplaires vendus. function bestSellingBooks ( seuil ) { return bookList . filter ( function ( book ) { return book . sales >= threshold ; } ); }

Le functionmot clé est utilisé ici au lieu de lambda, et une Array.filterméthode [8] au lieu d’une fonction globale filter, 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 l’exemple suivant :

// Renvoie une fonction qui se rapproche de la dérivée de f // en utilisant un intervalle de dx, qui doit être suffisamment petit. fonction dérivée ( f , dx ) { fonction de retour ( x ) { retour ( f ( x + dx ) – f ( x )) / dx ; } ; }

Parce que la fermeture dans ce cas survit à l’exécution de la fonction qui la crée, les variables fet dxvivent après le derivativeretour 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 du cadre de pile où cette variable est déclarée. Dans les langages avec des fermetures, les variables doivent continuer à exister tant que les fermetures existantes y font référence. Ceci est le plus souvent mis en œuvre à l’aide d’une certaine forme de récupération de place .

Représentation de l’État

Une fermeture peut être utilisée pour associer une fonction à un ensemble de variables « privées », qui persistent sur plusieurs invocations de la fonction. La portée de la variable englobe uniquement la fonction de fermeture, 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 analogues à un type d’ objet , en particulier les objets de fonction , avec une seule méthode publique (appel de fonction) et éventuellement de nombreuses variables privées (les variables fermées) .

Dans les langages avec état, les fermetures peuvent ainsi être utilisées pour implémenter des paradigmes de représentation d’état et de masquage d’informations , puisque les valeurs positives de la fermeture (ses variables fermées) sont d’ étendue indéfinie , de sorte qu’une valeur établie dans une invocation reste disponible dans la suivante. Les fermetures ainsi utilisées n’ont plus de transparence référentielle , et ne sont donc plus de pures fonctions ; néanmoins, ils sont couramment utilisés 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 en privé en modifiant cet environnement. Dans le schéma :

( définir foo #f ) ( définir la barre #f ) ( let (( secret-message “none” )) ( set! foo ( lambda ( msg ) ( set! secret-message msg ))) ( set! bar ( lambda () secret-message ))) ( affichage ( barre )) ; imprime “none” ( newline ) ( foo “rejoignez-moi sur les quais à minuit” ) ( display ( bar )) ; imprime “rencontrez-moi par les quais à minuit”

  • Les fermetures peuvent être utilisées pour implémenter des systèmes d’ objets . [9]

Remarque : Certains locuteurs appellent toute structure de données qui lie un environnement lexical une fermeture, mais le terme se réfère généralement 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 , plus une représentation de l’environnement lexical de la fonction (c’est-à-dire l’ensemble des variables disponibles) au moment où la fermeture a été créée. 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 elle-même. Lorsque la fermeture est entrée 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 ces 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 sorte qu’elles persistent 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 afin qu’elles survivent jusqu’à ce que toutes les fermetures les référençant ne soient plus utilisées.

Cela explique pourquoi, généralement, les langages qui prennent en charge nativement les fermetures utilisent également la récupération de place . 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ée), ou, si vous utilisez l’allocation de pile, pour que le langage accepte que certains cas d’utilisation conduisent à un comportement indéfini , en raison de pointeurs suspendus vers les variables automatiques libérées, comme dans les expressions lambda en C++11 [10] ou les fonctions imbriquées en GNU C. [11] Le problème funarg (ou problème “argument fonctionnel”) décrit la difficulté d’implémenter des fonctions comme objets de première classe dans une pile langage de programmation basé sur le C comme C ou C++. De même en Dversion 1, 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 encore de nombreux modèles fonctionnels utiles, mais pour les cas complexes, il faut une Allocation de tas explicite pour les variables. La version 2 de D a résolu ce problème en détectant quelles variables doivent être stockées sur le tas et effectue une allocation automatique. Étant donné que D utilise la récupération de place, dans les deux versions, il n’est pas nécessaire de suivre l’utilisation des variables lorsqu’elles sont transmises.

Dans les langages fonctionnels stricts avec des données immuables ( eg 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 cycle possible dans les références des variables. Par exemple, dans Erlang, tous les arguments et variables sont alloués sur le tas, mais leurs références sont en outre 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 ramasse-miettes incrémentiel.

En 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 qui est invisible pour le programmeur.

Scheme , qui a un système de portée lexicale de type ALGOL avec des variables dynamiques et une récupération de place, n’a pas de modèle de programmation de 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 contient le code et les variables libres de son environnement persistent dans le programme tant qu’elles sont accessibles, et peuvent donc être utilisées aussi librement que n’importe quelle autre expression Scheme. [ citation nécessaire ]

Les fermetures sont étroitement liées aux acteurs dans le modèle d’acteur de calcul simultané 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 simultanés est de savoir si les variables d’une fermeture peuvent être mises à jour et, si c’est le cas, comment ces mises à jour peuvent être synchronisées. Les acteurs apportent une solution. [12]

Les fermetures sont étroitement liées aux objets fonctionnels ; la transformation de la première à la seconde est connue sous le nom de défonctionnalisation ou de lambda lifting ; voir aussi conversion de fermeture . [ citation nécessaire ]

Différences de sémantique

Environnement lexical

Comme différentes langues 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 aussi ce que les fermetures dans n’importe quelle langue doivent capturer. Cependant la signification d’une variablela reliure 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 de l’emplacement lié peut le faire. Dans de tels 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. Ceci est souvent appelé capturer 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 ; fonction foo () { var x ; f = fonction () { retour ++ x ; } ; g = fonction () { retour — x ; } ; x = 1 ; alert ( ‘à l’intérieur de foo, appel à f() : ‘ + f ()); } foo (); // 2 alert ( ‘appel à g(): ‘ + g ()); // 1 (–x) alerte ( ‘appel à g(): ‘ + g ()); // 0 (–x) alerte ( ‘appel à f(): ‘ + f ()); // 1 (++x) alerte ( ‘appel à f(): ‘ + f ()); // 2 (++x)

La fonction fooet les fermetures référencées par des variables fet gutilisent toutes 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 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

[13]

var module = { x : 42 , getX : function () { renvoie ceci . x ; } } var unboundGetX = module . obtenirX ; consoler . log ( unboundGetX ()); // La fonction est invoquée à la portée globale // émet indéfini car ‘x’ n’est pas spécifié dans la portée globale. var liéGetX = unboundGetX . lier ( module ); // spécifiez le module d’objet comme console de fermeture . 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 parce que la variable ‘e’ est liée à la portée ci-dessus et évaluée paresseusement au clic, ce qui se passe réellement est que chaque événement on click émet l’id du dernier élément dans ‘elements’ lié à la fin de la boucle for. [14]

var éléments = document . getElementsByTagName ( ‘a’ ); //Incorrect : e est lié à la fonction contenant la boucle ‘for’, et non à la fermeture de “handle” for ( var e of elements ){ e . onclick = fonction handle (){ alert ( e . id );} }

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

D’autre part, de nombreux langages fonctionnels, tels que ML , lient directement les variables aux valeurs. Dans ce cas, puisqu’il n’y a aucun moyen de changer 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 capturer 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 vous 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.

Encore un autre sous-ensemble, les langages fonctionnels paresseux tels que Haskell , lient les variables aux résultats des calculs futurs plutôt qu’aux valeurs. Considérez cet exemple dans Haskell :

— Haskell foo :: Fractionnaire a => a -> a -> ( a -> a ) foo x y = ( z -> z + r ) où r = x / y f :: Fractionnel a => a -> a f = foo 1 0 principal = imprimer ( f 123 )

La liaison de rcapturée par la fermeture définie dans la fonction fooest liée au calcul (x / y), qui dans ce cas entraîne 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 et tente en fait d’utiliser la liaison capturée.

Fermeture départ

Pourtant, d’autres différences se manifestent dans le comportement d’autres constructions à portée lexicale, telles que return, breaket les continuedéclarations. De telles constructions peuvent, en général, être considérées comme invoquant 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 en boucle soient considérées en termes d’appels de fonction récursifs). Dans certains langages, comme 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ée. Cependant, dans Smalltalk , l’opérateur superficiellement similaire^appelle 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 suite 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. Les exemples suivants dans ECMAScript et Smalltalk mettent en évidence la différence :

“Smalltalk” foo | xs | xs := #( 1 2 3 4 ) . xs faire : [ : x | ^ x ] . ^ 0 bar Transcription show: ( self foo printString ) “prints 1” // Fonction ECMAScript foo () { var xs = [ 1 , 2 , 3 , 4 ]; xs . forEach ( fonction ( x ) { retourner x ; }); retourne 0 ; } alerte ( foo ()); // imprime 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 xlaissera la fermeture interne pour commencer une nouvelle itération de la forEachboucle, alors que dans l’exemple Smalltalk, ^xabandonnera la boucle et reviendra de la méthode foo.

Common Lisp fournit une construction qui peut exprimer l’une ou l’autre 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 suite d’échappement capturée de survivre dans la mesure où elle peut être invoquée avec succès. Considérer:

“Smalltalk” toto ^ [ : x | ^ x ] barre | f | f := soi foo . valeur f : 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 la méthode Smalltalk ne suit pas la discipline de la pile spaghetti pour faciliter les retours multiples, cette opération entraîne une erreur.

Certains langages, tels que Ruby , permettent au programmeur de choisir la manière dont returnil est capturé. Un exemple en Ruby :

# Rubis # Fermeture en utilisant un Proc def foo f = Proc . new { return “revenir de foo depuis l’intérieur de la procédure” } f . call # control laisse foo ici return “return from foo” end # Fermeture à l’aide d’une barre de définition lambda f = lambda { return ” return from lambda” } f . call # control ne quitte pas la barre ici return “retour de la barre” end met foo # imprime “retour de foo depuis l’intérieur de la procédure” met bar # imprime “retour de bar”

Les deux Proc.newet lambdadans cet exemple sont des façons 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 call/cc call-with-current-continuation ) ( define ( foo ) ( call/cc ( lambda ( return ) ( define ( f ) ( return “return from foo from inside proc” )) ( f ) ; control left foo here ( return “return from foo” )))) ( définir ( bar ) ( call/cc ( lambda ( return ) ( define ( f ) ( call/cc ( lambda ( return ) ( return “return from lambda” )))) ( f ) ; le contrôle ne laisse pas bar ici ( retour “retour de la barre” )))) ( affichage ( foo )) ; imprime “return from foo from inside proc” ( newline ) ( display ( bar )) ; imprime “retour du bar”

Constructions de type fermeture

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

Rappels (C)

Certaines bibliothèques C prennent en charge les rappels . Ceci 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 séparé vers des données arbitraires au choix de 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 se référer aux informations capturées au moment où il a été enregistré auprès de la bibliothèque. L’idiome est similaire aux fermetures dans la fonctionnalité, mais pas dans la 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 GUI Widget pour implémenter la programmation pilotée par les événements en associant les fonctions générales des widgets graphiques (menus, boutons, cases à cocher, curseurs, spinners, etc.) avec 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 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, il peut produire un résultat correct s’il est compilé sans optimisation, c’est-à-dire à -O0) :

#include <stdio.h> typedef int ( * fn_int_to_int )( int ); // type de fonction int->int additionneur fn_int_to_int ( nombre int ) { int add ( int value ) { valeur de retour + nombre ; } retourner & ajouter ; // L’opérateur & est facultatif ici car le nom d’une fonction en C est un pointeur pointant sur lui-même } int principal ( vide ) { fn_int_to_int add10 = additionneur ( 10 ) ; printf ( “%d n ” , add10 ( 1 )); retourne 0 ; }

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

#include <stdio.h> int principal ( vide ) { typedef int ( * fn_int_to_int )( int ); // type de fonction int->int fn_int_to_int adder ( int number ) { int add ( int value ) { valeur de retour + nombre ; } retour ajouter ; } fn_int_to_int add10 = additionneur ( 10 ) ; printf ( “%d n ” , add10 ( 1 )); retourne 0 ; }

S’il est exécuté, cela s’imprime maintenant 11comme prévu.

Classes locales et fonctions lambda (Java)

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

class CalculationWindow étend JFrame { résultat int volatil privé ; // … public void calculateInSeparateThread ( URI final 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 : résultat = résultat + 10 ; } } ). commencer (); } }

La capture de finalvariables permet de capturer des variables par valeur. Même si la variable que vous souhaitez capturer est non- final, vous pouvez toujours la copier 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 modifiable, par exemple, un tableau à un seul élément. La classe locale ne pourra pas modifier la valeur de la référence du conteneur elle-même, mais elle pourra modifier le contenu du conteneur.

Avec l’avènement des expressions lambda de Java 8, [15] la fermeture entraîne l’exécution du code ci-dessus comme suit :

class CalculationWindow étend JFrame { résultat int volatil privé ; // … public void calculateInSeparateThread ( URI final uri ) { // Le code () -> { /* code */ } est une fermeture. nouveau Thread (() -> { calculer ( uri ); résultat = résultat + 10 ; }). commencer (); } }

Les classes locales sont l’un des types de classe interne qui sont déclarés dans le corps d’une méthode. Java prend également en charge les classes internes qui sont déclarées comme membres non statiques d’une classe englobante. [16] Ils sont normalement appelés simplement “classes internes”. [17] Celles-ci 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. [18]

public class EnclosingClass { /* Définit la classe interne */ public class InnerClass { public in incrementAndReturnCounter () { return counter ++ ; } } comptoir int privé ; { compteur = 0 ; } public int getCounter () { renvoie le compteur ; } public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); /* Instancie la classe interne, avec liaison à l’instance */ EnclosingClass . InnerClass innerClassInstance = enclosingClassInstance . new InnerClass (); for ( int i = enclosingClassInstance . getCounter ( ) ; ( i = innerClassInstance . _ _ _ _ _ _ _ _ dehors . println ( je ); } } }

A l’exécution, cela imprimera les entiers de 0 à 9. Attention à ne pas confondre ce type de classe avec la classe imbriquée, qui se déclare de la même manière avec un usage accompagné du modificateur “static” ; ceux-ci n’ont pas l’effet souhaité mais sont plutôt des classes sans liaison spéciale définie dans une classe englobante.

À partir de 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 étant 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 ) { Function < String , Integer > length = s -> s . longueur (); Système . dehors . println ( length . apply ( “Hello, world!” ) ); // Imprimera 13. }

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

Apple a introduit les blocs , une forme de fermeture, en tant qu’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 les blocs et les littéraux de bloc sont marqués par ^. Les variables locales normales sont capturées par valeur lors de la création du bloc et sont en lecture seule à l’intérieur du bloc. Les variables à capturer par référence sont marquées par __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. [19] [20]

typedef int ( ^ IntBlock )(); IntBlock downCounter ( int start ) { __block int je = début ; retourner [[ ^ entier () { retour je — ; } copier ] autorelease ] ; } 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 = nouveau [] { 1 , 2 , 3 , 4 } ; var multiplicateur = 2 ; var résultat = données . Sélectionnez ( 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 fermetures :

Dim data = { 1 , 2 , 3 , 4 } Dim multiplicateur = 2 Dim result = data . Sélectionnez ( Fonction ( x ) x * multiplicateur )

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

test automatique1 () { int a = 7 ; return délégué () { return a + 3 ; } ; // construction de délégué anonyme } test automatique2 () { int a = 20 ; int foo () { renvoie un + 5 ; } // retour de la fonction interne & 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 }

D version 1, 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 en appelant foo via dg(), renverra un ‘ aléatoire’ entier). 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, tant qu’elles ne sont utilisées que pendant que les valeurs référencées sont toujours valides (par exemple, appeler 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 , dans la 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 l’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 de inner qui référencent les variables d’une fonction.

Objets 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 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 sort de la portée d’un objet référencé, invoquer sonoperator()provoque un comportement indéfini car les fermetures C++ ne prolongent pas la durée de vie de leur contexte.

void foo ( string myname ) { int y ; vecteur < chaîne > n ; // … auto i = std :: find_if ( n . begin (), n . end (), // c’est l’expression lambda : [ & ]( const string & s ) { return s != myname && s . taille () > y ; } ); // ‘i’ est maintenant soit ‘n.end()’ soit pointe sur 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 les 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_bouton . click_event . subscribe ( agent ( x , y : INTEGER ) do map . country_at_coordinates ( x , y ). display end )

l’argument to subscribeest un agent, représentant une procédure à 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 transmises 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 les variables locales de la portée englobante. Cette décision de conception permet d’éviter toute ambiguïté lorsque l’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 à thisen Java), ses fonctionnalités et les arguments de l’agent lui-même 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.

C++Builder __closure mot réservé

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

En C standard, vous pouvez é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, vous pouvez déclarer un typedef pour un pointeur vers une méthode en utilisant la syntaxe suivante :

typedef void ( __closure * TMyMethodPointer )();

Voir également

  • Fonction anonyme
  • Blocs (extension du langage C)
  • Modèle de commande
  • Continuation
  • Curry
  • Problème funarg
  • Calcul lambda
  • Évaluation paresseuse
  • Application partielle
  • Pile de spaghettis
  • Fermeture syntaxique
  • Programmation au niveau de la valeur

Remarques

  1. ^ La fonction peut être stockée en tant que référence à une fonction, telle qu’un pointeur de fonction .
  2. ^ Ces noms font le plus souvent référence à des valeurs, des variables mutables ou des fonctions, mais peuvent également être d’autres entités telles que des constantes, des types, des classes ou des étiquettes.

Références

  1. ^ Sussman et Steele. “Schéma: Un interpréteur pour le calcul lambda étendu”. “… une structure de données contenant une expression lambda et un environnement à utiliser lorsque cette expression lambda est appliquée aux arguments.” ( Wikisource )
  2. ^ David A. Turner (2012). “Un peu d’histoire des Langages de programmation fonctionnels” . Tendances en programmation fonctionnelle ’12. La section 2, note 8 contient l’affirmation sur les expressions M.
  3. ^ PJ Landin (1964), L’évaluation mécanique des expressions
  4. ^ Joel Moses (juin 1970), La fonction de la fonction dans LISP, ou pourquoi le problème FUNARG devrait être appelé le problème de l’environnement , hdl : 1721.1/5854 , AI Memo 199, Une métaphore utile pour la différence entre FUNCTION et QUOTE en LISP est de considérer QUOTE comme une couverture poreuse ou ouverte de la fonction puisque les variables libres s’échappent dans l’environnement actuel. FUNCTION agit comme un revêtement fermé ou non poreux (d’où le terme “fermeture” utilisé par Landin). Ainsi, nous parlons d’expressions Lambda “ouvertes” (les fonctions dans LISP sont généralement des expressions Lambda) et d’expressions Lambda “fermées”. […] Mon intérêt pour le problème de l’environnement a commencé lorsque Landin, qui avait une profonde compréhension du problème, a visité le MIT en 1966-1967. J’ai ensuite réalisé la correspondance entre les listes FUNARG qui sont les résultats de l’évaluation des expressions Lambda “fermées” en LISP et les Lambda Closures d’ ISWIM .
  5. ^ Åke Wikström (1987). Programmation fonctionnelle à l’aide de ML standard . ISBN 0-13-331968-7. La raison pour laquelle on l’appelle une “fermeture” est qu’une expression contenant des variables libres est appelée une expression “ouverte”, et en lui associant les liaisons de ses variables libres, vous la fermez.
  6. ^ Gerald Jay Sussman et Guy L. Steele, Jr. (décembre 1975), Scheme: An Interpreter for the Extended Lambda Calculus , AI Memo 349
  7. ^ Abelson, Harold; Susman, Gérald Jay ; Susman, Julie (1996). Structure et interprétation des programmes informatiques . Cambridge, Massachusetts : MIT Press. p. 98–99. ISBN 0262510871.
  8. ^ “tableau.filtre” . Centre de développement Mozilla . 10 janvier 2010 . Récupéré le 9 février 2010 .
  9. ^ “Re: FP, OO et relations. Est-ce que quelqu’un l’emporte sur les autres?” . 29 décembre 1999. Archivé de l’original le 26 décembre 2008 . Récupéré le 23 décembre 2008 .
  10. ^ Comité des normes Lambda Expressions and Closures C ++ . 29 février 2008.
  11. ^ Manuel GCC, 6.4 Fonctions imbriquées , “Si vous essayez d’appeler la fonction imbriquée via son adresse après la sortie de la fonction contenante, l’enfer se déchaîne. Si vous essayez de l’appeler après la sortie d’un niveau de portée contenant, et s’il fait référence à certains des variables qui ne sont plus dans la portée, vous avez peut-être de la chance, mais il n’est pas sage de prendre le risque. Si, toutefois, la fonction imbriquée ne fait pas référence à quelque chose qui est sorti de la portée, vous devriez être en sécurité.
  12. ^ Les fondements de la sémantique des acteurs s’accrocheront. Thèse de doctorat en mathématiques du MIT. juin 1981.
  13. ^ “Fonction.prototype.bind()” . Documents Web MDN . Récupéré le 20 novembre 2018 .
  14. ^ “Fermetures” . Documents Web MDN . Récupéré le 20 novembre 2018 .
  15. ^ “Expressions Lambda (Les Tutoriels Java)” .
  16. ^ “Classes imbriquées, internes, membres et de niveau supérieur” .
  17. ^ “Exemple de classe interne (Les didacticiels Java> Apprendre le langage Java> Classes et objets)” .
  18. ^ “Classes imbriquées (Les didacticiels Java> Apprendre le langage Java> Classes et objets)” .
  19. ^ Apple Inc. “Bloque les sujets de programmation” . Récupéré le 8 mars 2011 .
  20. ^ Joachim Bengtsson (7 juillet 2010). “Programmation avec les blocs C sur les appareils Apple” . Archivé de l’original le 25 octobre 2010 . Récupéré le 18 septembre 2010 .
  21. ^ Une documentation complète peut être trouvée sur http://docwiki.embarcadero.com/RADStudio/Rio/en/Closure

Liens externes

  • “Lambda Papers” originaux : Une série classique d’articles de Guy Steele et Gerald Sussman discutant, entre autres, de la polyvalence des fermetures dans le contexte de Scheme (où elles apparaissent comme des expressions lambda ).
  • Neal Gafter (28 janvier 2007). “Une définition des fermetures” .
  • Gilad Bracha , Neal Gafter , James Gosling , Peter von der Ahé . “Fermetures pour le langage de programmation Java (v0.5)” .{{cite web}}: Maint CS1 : noms multiples : liste des auteurs ( lien )
  • Closures : Un article sur les fermetures dans les langages impératifs à typage dynamique , par Martin Fowler .
  • Méthodes de fermeture de collection : Un exemple d’un domaine technique où l’utilisation de fermetures est commode, par Martin Fowler.
You might also like
Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More