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:
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.
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.
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 :
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.
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
end
C'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
end
Cette fonction marche également :
[bill, maud] == selectionAnciennete(employes, 1)
Si nous cherchons à généraliser cette conception, plusieurs problèmes se posent :
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
end
Pour 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
end
class PredicatEmployeAnciennete < PredicatEmploye
def initialize(min)
@min = min
end
def estValide(employe)
employe.anciennete >= @min
end
end
Ces 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
end
Aprè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) }
end
La 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 }
end
Dé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
end
Exemple 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) }
end
ce qui permet la combinaison suivante :
pred3 = predicatOu(pred1, pred2)
[john, bill, maud] == selection(employes, pred3)
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 :
..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} "}