J'ai récemment suivi un cours en ligne sur les langages de programmation dans lequel, entre autres concepts, des fermetures ont été présentées. J'écris deux exemples inspirés de ce cours pour donner un peu de contexte avant de poser ma question.
Le premier exemple est une fonction SML qui produit une liste des nombres de 1 à x, où x est le paramètre de la fonction:
fun countup_from1 (x: int) =
let
fun count (from: int) =
if from = x
then from :: []
else from :: count (from + 1)
in
count 1
end
Dans le SML REPL:
val countup_from1 = fn : int -> int list
- countup_from1 5;
val it = [1,2,3,4,5] : int list
La countup_from1
fonction utilise la fermeture d'aide count
qui capture et utilise la variable à x
partir de son contexte.
Dans le deuxième exemple, lorsque j'appelle une fonction create_multiplier t
, je récupère une fonction (en fait, une fermeture) qui multiplie son argument par t:
fun create_multiplier t = fn x => x * t
Dans le SML REPL:
- fun create_multiplier t = fn x => x * t;
val create_multiplier = fn : int -> int -> int
- val m = create_multiplier 10;
val m = fn : int -> int
- m 4;
val it = 40 : int
- m 2;
val it = 20 : int
Donc, la variable m
est liée à la fermeture retournée par l'appel de fonction et maintenant je peux l'utiliser à volonté.
Maintenant, pour que la fermeture fonctionne correctement tout au long de sa durée de vie, nous devons prolonger la durée de vie de la variable capturée t
(dans l'exemple, il s'agit d'un entier mais il peut s'agir d'une valeur de n'importe quel type). Pour autant que je sache, en SML, cela est rendu possible par la collecte des ordures: la fermeture conserve une référence à la valeur capturée qui est ensuite éliminée par le garbage collector lorsque la fermeture est détruite.
Ma question: en général, la collecte des ordures est-elle le seul mécanisme possible pour garantir la sécurité des fermetures (appelables pendant toute leur durée de vie)?
Ou quels sont les autres mécanismes qui pourraient garantir la validité des fermetures sans ramassage des ordures: copier les valeurs capturées et les stocker dans la fermeture? Restreindre la durée de vie de la fermeture elle-même afin qu'elle ne puisse pas être invoquée après l'expiration de ses variables capturées?
Quelles sont les approches les plus populaires?
ÉDITER
Je ne pense pas que l'exemple ci-dessus puisse être expliqué / implémenté en copiant la ou les variables capturées dans la fermeture. En général, les variables capturées peuvent être de tout type, par exemple, elles peuvent être liées à une très grande liste (immuable). Ainsi, dans l'implémentation, il serait très inefficace de copier ces valeurs.
Par souci d'exhaustivité, voici un autre exemple utilisant des références (et des effets secondaires):
(* Returns a closure containing a counter that is initialized
to 0 and is incremented by 1 each time the closure is invoked. *)
fun create_counter () =
let
(* Create a reference to an integer: allocate the integer
and let the variable c point to it. *)
val c = ref 0
in
fn () => (c := !c + 1; !c)
end
(* Create a closure that contains c and increments the value
referenced by it it each time it is called. *)
val m = create_counter ();
Dans le SML REPL:
val create_counter = fn : unit -> unit -> int
val m = fn : unit -> int
- m ();
val it = 1 : int
- m ();
val it = 2 : int
- m ();
val it = 3 : int
Ainsi, les variables peuvent également être capturées par référence et sont toujours actives une fois l'appel de fonction qui les a créées ( create_counter ()
) terminé.
Réponses:
Le langage de programmation Rust est intéressant à cet égard.
Rust est un langage système, avec un GC en option, et a été conçu avec des fermetures depuis le début.
Comme les autres variables, les fermetures à la rouille se présentent sous différentes saveurs. Les fermetures de pile , les plus courantes, sont à usage unique. Ils vivent sur la pile et peuvent référencer n'importe quoi. Les fermetures en propriété prennent possession des variables capturées. Je pense qu'ils vivent sur le soi-disant «tas d'échange», qui est un tas mondial. Leur durée de vie dépend de qui les possède. Les fermetures gérées vivent sur le tas local de la tâche et sont suivies par le GC de la tâche. Je ne suis pas sûr de leurs limites de capture, cependant.
la source
Malheureusement, commencer par un GC fait de vous une victime du syndrome XY:
Notez, cependant, que l'idée d' étendre la durée de vie d'une variable n'est pas nécessaire pour une fermeture; c'est juste apporté par le GC; la déclaration de sécurité d'origine est juste les variables fermées devraient vivre aussi longtemps que la fermeture (et même cela est fragile, nous pourrions dire qu'elles devraient vivre jusqu'à la dernière invocation de la fermeture).
Il y a, essentiellement, deux approches que je peux voir (et elles pourraient potentiellement être combinées):
Ce dernier n'est qu'une approche symétrique. Il n'est pas souvent utilisé, mais si, comme Rust, vous avez un système de type sensible à la région, c'est certainement possible.
la source
Le ramasse-miettes n'est pas nécessaire pour les fermetures sécurisées, lors de la capture de variables par valeur. Un exemple frappant est C ++. C ++ n'a pas de ramasse-miettes standard. Les lambdas en C ++ 11 sont des fermetures (elles capturent des variables locales de la portée environnante). Chaque variable capturée par un lambda peut être spécifiée pour être capturée par valeur ou par référence. S'il est capturé par référence, vous pouvez dire qu'il n'est pas sûr. Cependant, si une variable est capturée par valeur, elle est sûre, car la copie capturée et la variable d'origine sont séparées et ont des durées de vie indépendantes.
Dans l'exemple SML que vous avez donné, il est simple à expliquer: les variables sont capturées par valeur. Il n'est pas nécessaire de "prolonger la durée de vie" d'une variable car vous pouvez simplement copier sa valeur dans la fermeture. Ceci est possible car, en ML, les variables ne peuvent pas être affectées à. Il n'y a donc pas de différence entre une copie et de nombreuses copies indépendantes. Bien que SML possède un garbage collection, il n'est pas lié à la capture de variables par des fermetures.
La récupération de place n'est pas non plus nécessaire pour les fermetures sécurisées lors de la capture de variables par référence (type de). Un exemple est l'extension Apple Blocks aux langages C, C ++, Objective-C et Objective-C ++. Il n'y a pas de garbage collection standard en C et C ++. Les blocs capturent les variables par valeur par défaut. Cependant, si une variable locale est déclarée avec variable dans le tas, et que le bloc gère sa mémoire, je crois grâce au comptage des références.
__block
, les blocs les capturent apparemment "par référence" et ils sont sûrs - ils peuvent être utilisés même après la portée dans laquelle le bloc a été défini. Ce qui se passe ici, c'est que les__block
variables sont en fait un structure spéciale en dessous, et lorsque les blocs sont copiés (les blocs doivent être copiés afin de les utiliser en dehors de la portée en premier lieu), ils "déplacent" la structure pour le__block
la source
ref
's'). Donc, OK, on peut débattre si la mise en œuvre des fermetures est liée à la collecte des ordures ou non, mais les déclarations ci-dessus doivent être corrigées.ref
s, tableaux, etc.) qui pointent vers une structure. Mais la valeur est la référence elle-même, pas la chose vers laquelle elle pointe. Si vous en avezvar a = ref 1
et que vous en faites une copievar b = a
et que vous l'utilisezb
, cela signifie-t-il que vous l'utilisez toujoursa
? Vous avez accès à la même structure pointée para
? Oui. Voilà comment ces types fonctionnent en SML et n'ont rien à voir avec les fermeturesLa collecte des ordures n'est pas nécessaire pour mettre en œuvre des fermetures. En 2008, le langage Delphi, qui n'est pas ramassé, a ajouté une implémentation de fermetures. Cela fonctionne comme ceci:
Le compilateur crée un objet fonctor sous le capot qui implémente une interface représentant une fermeture. Toutes les variables locales fermées sont changées des sections locales pour la procédure englobante aux champs de l'objet functor. Cela garantit que l'état est conservé aussi longtemps que le foncteur l'est.
La limitation de ce système est que tout paramètre transmis par référence à la fonction englobante, ainsi que la valeur de résultat de la fonction, ne peuvent pas être capturés par le foncteur car ce ne sont pas des locaux dont la portée est limitée à celle de la fonction englobante.
Le foncteur est désigné par la référence de fermeture, en utilisant du sucre syntaxique pour le faire ressembler au développeur comme un pointeur de fonction au lieu d'une interface. Il utilise le système de comptage de références de Delphi pour les interfaces afin de garantir que l'objet functor (et tout l'état qu'il contient) reste "vivant" aussi longtemps qu'il le faut, puis il est libéré lorsque le décompte tombe à 0.
la source
shared_ptr
n'est pas déterministe car les destructeurs courent pour décrémenter à zéro.