Styles

jeudi 9 décembre 2010

Closures - Partie 1

Alors que certains langages se posent encore des questions existentielles sur l'incorporation ou pas (et à quelle date) de closures, cette capacité d'un langage de haut niveau est largement déployée au sein des langages dits dynamiques (moins péjoratif que langage 'script'), sans parler des langages fonctionnels pour lesquels les closures sont aussi naturelles que les fonctions d'ordre supérieur.

Sur le Web, il n'est pas rare de voir des exemples explicatifs de ce que sont les closures, mais plus rarement des exemples d'application des closures à des cas concrets. Je vais, dans plusieurs billets de ce blog, essayer de vous fournir un aperçu de cas concrets où l'utilisation des closures est intéressante.

Avant de commencer, il est bien sur nécessaire de clarifier ce qu'on entend par closure.

Au départ étaient les mathématiques et tout était clair et non-ambigüe. Ils donnèrent au nom de 'fermeture' (closure)une définition algébrique : Un ensemble d'éléments est dit fermé (closed) sous une certaine opération si l'application de cette opération aux éléments de l'ensemble produit un élément qui est encore un élément de l'ensemble.

Puis vinrent les informaticiens qui par leur nature fainéante, firent des raccourcis honteux : la communauté Lisp employa le mot closure pour décrire un concept complètement différent : une closure est une technique d'implémentation pour représenter des procédures ayant des variables libres.

De cette non-conformité au concept mathématique provient une partie de l'incompréhension qu'ont la plupart des programmeurs de base. Et puisqu'il faut rester concret quand on fait de l'informatique, voici l'exemple de base pour créer une closure, qu'on retrouve dans tous les articles d'introduction. L'exemple est en Python 2.7 :
def multiplier(nb):
def multi(value):
return value * nb
return multi
Ici, multi est la closure, multiplier est la fonction fabricant la closure. La plupart du temps, il s'agit d'encapsuler dans une fonction la création de la closure. La closure dispose alors des variables locales de la fonction dans laquelle elle a été créée. On peut appeler multiplier, l'usine à closures ('closure factory').

On peut utiliser cette closure de cette façon :
>>> multi10 = multiplier(10)
>>> multi10(2)
20
C'est un peut comme si on appelait l'usine à fabriquer des closures pour configurer au moment de l'exécution (runtime) une fonction particulière et d'en produire donc de toute sorte.

Comme il s'agit ici d'encloisonner des variables dans un scope particulier à la closure, on peut très bien définir le même comportement en utilisant la notion de classe classique. L'exemple ci-dessous converti en classe donne :
class MultiplierFactory:
def __init__(self, nb):
self.nb = nb
def multiply(self, value):
return self.nb * value

>>> multi10 = MultiplierFactory(10).multiply
>>> multi10(2)
20

C'est une façon de créer des pseudo-classes anonymes, sachant qu'une classe ne sert qu'à gérer la 'globalité' de certaines variables. Pour continuer le parallèle entre une closure et une classe, il va être possible, de façon beaucoup plus succincte d'exprimer des modèles de conception comportementaux en closure.

Une closure n'est pas simplement une fonction anonyme qu'on se passe de variable en variable. Pour réellement parler de closure, il faut qu'il y ait en jeu des variables libres telles que nb dans l'exemple précédent.

A noter aussi qu'une closure ne s'exécute qu'au dernier moment, lors de son appel explicite. La configuration de la fonction n'a pas nécessité de faire fonctionner du code source de cette fonction.

C'est par exemple, intéressant quand on veut programmer par évènements et appels en retour (callbacks). Par exemple, sans closure, on pourrait essayer de coder quelque chose comme ça, mais ce code ne peut pas marcher :
from threading import Timer

def show_message(msg):
print msg

def set_alarm(msg, timeout):
Timer(timeout, show_message(msg))

>>> set_alarm("Réveille Toi !", 3)
Réveille Toi ! ---> s'affiche instantanément
En effet, Timer(timeout, show_message(msg)) va appeler show_message(msg) avant d'appeler Timer. Timer va donc être appelé avec le résultat de l'appel à show_message(msg), ce qui n'est pas ce que l'on veut. Par contre, avec une closure, on peut écrire le code de cette manière :
from threading import Timer

def set_alarm(msg, timeout):
def show_message():
print msg
t = Timer(timeout, show_message)
t.start()

>>> set_alarm("Réveille Toi !", 3)

Au bout de 3 secondes on a :
>>> Réveille Toi !
Ce bout de code permet d'éviter l'emploi d'une variable globale msg, qu'il serait alors difficile de maintenir cohérente dans un environnement multithread par exemple.

Aucun commentaire: