Trois cas d'usage des fermetures
Un des avantages souvent noté dans les langages "dynamiques" de la lignée de perl : python, ruby, groovy, etc. est la notion de fermeture. Qu'est-ce qu'une fermeture et à quoi cela sert-il ? Essayons cette définition (issue de ce post très intéressant de Neal Gafter, qui cherche à les introduire dans Java : a definition of closures ) :
Une fermeture est une fonction qui capture les liaisons à des variables libres dans son contexte lexical.
C'est une définition assez ingrate, mais plutôt que de tourner autour indéfiniment à la recherche d'une meilleure abstraction, prenons trois exemples, illustrant trois usages possibles des fermetures:
1) Cacher un état dans une fonction
Voici une fonction simple suivie d'un appel (en ruby) :
def montantTTC(montant)
montant * 1.196
end
mtFacture = montantTTC(48.07)
Ici le taux appliqué est constant. Supposons que nous voulions rendre le taux de TVA variable, et que pour une raison saugrenue nous ne voulions pas passer ce taux dans chaque expression où l'on calcule le montant TTC. Nous pourrions écrire :
def appliqueTVA(taux)
Proc.new { |montant| montant * (1 + taux) }
end
La fonction appliqueTVA retourne une fermeture, c'est à dire un bloc de code exécutable (délimité par les accolades) dans lequel les variables sont liées à un contexte d'exécution particulier.
- La variable montant est un paramètre du bloc; elle sera liée à la valeur fournie par l'appelant du bloc de code.
- La variable taux serait libre à l'intérieur du bloc, si elle n'était pas définie comme paramètre dans la fonction appliqueTVA. La valeur de cette variable sera donc fournie par l'appelant de la fonction appliqueTVA, et associée jusqu'à nouvel ordre au bloc de code.
montantTTC = appliqueTVA(0.196)
mtFacture = montantTTC.call(48.07)
Un appel à la fonction appliqueTVA avec la valeur 0.196 produit donc un bloc de code capable de calculer un montant TTC pour cette valeur précise de taux. On peut ranger ce bloc dans une variable afin de l'utiliser dans une autre partie du code de l'application, plus tard. L'exemple suivant montre l'intérêt de pouvoir séparer dans le temps la constitution d'un bloc de code et son exécution.
2) Différer l'évaluation d'une fonction :
On veut écrire, à l'aide d'une librairie spécialisée, un programme créant dynamiquement une IHM. Un code spécifique décrit les étapes de la construction de l'interface, ce que nous pourrions schématiser comme suit :
- construis un formulaire,
- inclus dans le formulaire trois champs d'édition,
- inclus des boutons libellés Valider et Annuler,
- lorsque le bouton Valider est cliqué, tu fais le traitement XYZ,
- lorsque le bouton Annuler est cliqué, tu refermes le formulaire.
Dans cette liste, les instructions 4 et 5 ont une portée fort différente des trois premières : elles n'instruisent plus la réalisation du formulaire, mais le comportement de celui-ci une fois réalisé et en cours d'utilisation. Pour définir ce comportement, le constructeur de l'IHM affecte à l'une des propriétés de l'IHM, un bloc de code, ce qui revient à dire :
sur l'évènement clic du bouton Valider tu appelleras ma procédure traitementXYZ
d'où le terme de callback, utilisé pour désigner ce type de mécanisme: un objet de haut niveau qui "passe" une de ses procédures à un objet de plus bas niveau afin que celui-ci le rappelle plus tard.
Voici un exemple d'utilisation de fermetures dans un programme ruby construisant une interface Tk
controleur = Controleur.new
...
traitementXYZ = Proc.new { controleur.valide(edMontant.text)
controleur.calculeXYZ }
annuler = Proc.new { exit }
...
btValider = TkButton.new(top, text => 'Valider',
command => traitementXYZ)
btAnnuler = TkButton.new(top, text => 'Annuler',
command => annuler)
...Ici, l'assignation du bloc de code au contrôle concerné se fait via sa propriété command.
3) Créer des fonctions de haut-niveau :
Les fermetures représentent un avantage majeur en termes de modularité du code, car elles permettent de créer des fonctions de haut-niveau, i.e. des fonctions ayant des fonctions comme paramètres en entrée ou en sortie.
Imaginons une liste d'employés :
class Employe
attr_reader :nom, :salaire
def initialize(nom, salaire, dateEntree)
@nom,@salaire,@dateEntree = nom,salaire,dateEntree
end
def anciennete
((Time.now - @dateEntree) / 31557600).floor
end
end
john = Employe.new("John", 40043, Time.mktime(2006,5,17))
bill = Employe.new("Bill", 34500, Time.mktime(2005,10,7))
maud = Employe.new("Maud", 35000, Time.mktime(2003,1,3))
employes = [john, bill, maud]..à partir de laquelle on souhaite effectuer des sélections, sur le salaire par exemple. Dans un monde sans fermeture, nous écririons :
def selectionSalaire(employes, min, max)
result = []
for employe in employes
result.push(employe) if employe.salaire >= min && employe.salaire < max
end
result
endC'est un code particulièrement lourd pour du ruby, mais néanmoins correct comme le montre l'égalité suivante :
[john, maud] == selectionSalaire(employes, 35000, 50000)
Imaginons une autre fonction de sélection, sur l'ancienneté, cette fois :
def selectionAnciennete(employes, min)
result = []
for employe in employes
result.push(employe) if employe.anciennete >= min
end
result
endCette fonction marche également :
[bill, maud] == selectionAnciennete(employes, 1)
Si nous cherchons à généraliser cette conception, plusieurs problèmes se posent :
- Le code d'extraction de la sélection est à chaque fois le même, à la condition près. Comment factoriser ce code ?
- Le code définissant le critère de sélection est entièrement couplé à la méthode de parcours de la liste. Comment découpler la constitution d'un prédicat de sélection et son utilisation ?
- Si nous enchaînons ensemble des sélections, nous ne pouvons obtenir que restrictions successives. Comment pourrions-nous combiner des critères entre eux?
Voici une solution sans fermeture: nous créons une classe de base pour les prédicats sur les employes :
class PredicatEmploye def estValide(employe) true end end
et nous modifions la fonction de sélection afin qu'elle utilise notre objet prédicat :
def selection(employes, predicat)
result = []
for employe in employes
result.push(employe) if predicat.estValide(employe)
end
result
endPour créer de nouveaux types de prédicats, il suffit de dériver cette classe :
class PredicatEmployeSalaire < PredicatEmploye
def initialize(min, max)
@min,@max = min,max
end
def estValide(employe)
employe.salaire >= @min && employe.salaire < @max
end
endclass PredicatEmployeAnciennete < PredicatEmploye
def initialize(min)
@min = min
end
def estValide(employe)
employe.anciennete >= @min
end
endCes classes permettent de vérifier par exemple :
pred1 = PredicatEmployeSalaire.new(35000, 50000)
pred2 = PredicatEmployeAnciennete.new(1)
[john, maud] == selection(employes, pred1)
[bill, maud] == selection(employes, pred2)Pour combiner des prédicats, il suffit de créer une nouvelle classe de prédicat :
class PredicatEmployeOU < PredicatEmploye
def initialize(predicatA, predicatB)
@predicatA, @predicatB = predicatA, predicatB
end
def estValide(employe)
@predicatA.estValide(employe) || @predicatB.estValide(employe)
end
endAprès vérification :
pred3 = PredicatEmployeOU.new(pred1,pred2)
[john, bill, maud] == selection(employes, pred3)Tout ceci fonctionne, mais c'est un peu lourd. Voici comment écrire un code plus concis, en tirant parti des fermetures:
Tout d'abord nous pourrions exploiter les blocs de code à l'intérieur de la fonction de sélection, ce qui soulagerait déja celle-ci de quelques lignes :
def selection(employes, predicat)
employes.select { |employe| predicat.estValide(employe) }
endLa fonction intégrée select parcourt une liste en exécutant pour chaque élément un bloc chargé de décider si l'élément doit être extrait ou non. La variable predicat présente dans la fermeture, a été liée à l'argument passé à la fonction selection, à savoir un objet muni d'une fonction de validation.
Mais allons plus loin : pourquoi écrire toute une classe, quand on peut écrire un simple bloc de code ?
def predicatSalaire(min, max)
Proc.new { |employe| employe.salaire >= min && employe.salaire < max }
end
def predicatAnciennete(min)
Proc.new { |employe| employe.anciennete >= min }
endDésormais la sélection est implémentée sans l'aide d'objet Predicat en passant simplement le bloc prédicat à la fonction filter (le préfixe & dénote un argument de type bloc) :
def selection(employes, predicat)
employes.select &predicat
endExemple d'utilisation :
pred1 = predicatSalaire(35000, 50000)
pred2 = predicatAnciennete(1)
[john, maud] == selection(employes, pred1)
[bill, maud] == selection(employes, pred2 )Pour combiner des prédicats, il suffit d'écrire une fonction créant un bloc de code dans lequel seront combinés les prédicats passés en arguments :
def predicatOu(predA,predB)
Proc.new { |employe| predA.call(employe) || predB.call(employe) }
endce qui permet la combinaison suivante :
pred3 = predicatOu(pred1, pred2)
[john, bill, maud] == selection(employes, pred3)Conclusion
Dans ce dernier exemple, les fermetures sont utilisées pour résoudre un problème de généralisation et de paramétrage d'un traitement. Face à ce type de problème, la conception objet conduit habituellement à une "réification" du traitement en un système de classes: des séquences d'instructions ont été transposées vers des structures de données spécifiques, ce qui permet effectivement un couplage faible (la fabrication des critères est bien séparée de leur utilisation) et une certaine versatilité, permise par le polymorphisme. En revanche, la simplicité et la concision sont perdues au passage.
Une design à base de fermetures atteint le même niveau de découplage, en transmettant, plutôt qu'une structure de données munie d'une fonction, une simple fonction munie d'un état. La liaison de variables au sein de la fermeture confère également à ce dispostif une très grande versatilité.
La différence entre les deux solutions, et l'avantage des fermetures à mons sens, tient dans la concision et la simplicité préservées du code.
Quand faut-il utiliser ces fonctions de haut-niveau ? A chaque fois que nous souhaitons :
- encapsuler un état dans une fonction,
- spécifier un traitement dont l'exécution doit être différée,
- plus généralement, découpler un traitement de son exécution
..sans avoir à créer de classes ou de structures de données supplémentaires, alors les fermetures constituent une solution adéquate et élégante.
Message = "merci pour les conseils sur ce post."
%w{ Bernard
Manu
Pierre
Gilles
Marc-Antoine
Philippe }.each { |prenom| print "#{prenom}, #{Message} "}