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.

jeudi 2 décembre 2010

Python en remplaçant de Perl

Je me suis mis récemment au langage python. J'avais des préjugés sur ce langage au moment où j'ai entendu dire qu'il fallait respecter la tabulation pour que le programme fonctionne. Je voyais là le signe d'un langage complètement arriéré, digne des plus beaux langages de grand papa, notamment COBOL qui obligeait à respecter la tabulation, la taille des lignes, des sections particulières...

Comment ce fait-il que des gens aient choisi un tel retour en arrière pour créer ce langage ? En fait, il s'agit surtout d'une incompréhension. La tabulation en elle-même peut être celle que l'on souhaite mais la tabulation sert à définir le début et la fin des blocs de lignes de codes. C'est en fait un moyen astucieux de définir des blocs sans avoir à utiliser de symbole particulier comme { } par exemple :
def fib(n):    # write Fibonacci series up to n
"""Print a Fibonacci series up to n."""
a, b = 0, 1
while a < n:
print a,
a, b = b, a+b
Moins je tape de caractère, mieux je me porte. C'est un principe fondamental de la programmation : le meilleur programme c'est celui qui n'a pas de ligne de code.

Après m'être auto-formé à python, je me suis aperçu que python est aussi souple que mon autre langage de prédilection : Perl. Python comme Perl, centralise les packages disponibles au sein d'un même répertoire appelé PyPI. Sous Perl, c'est le répertoire CPAN.

La différence philosophique entre Perl et Python se situe au niveau des slogans qu'ils utilisent : Perl a choisi le TMTOWTDI (There's more than one way to do it) tandis que Python, justement, est parti sur le slogan "There should not be more than one way to do it". Et finalement, sur le long terme, c'est payant pour Python. Le langage Python reste simple et évite toutes les tortures mentales introduites par Perl. Cela permet à plusieurs programmeurs de travailler sur un même code, la maintenance est facilitée. De plus, quand vous arrêté de faire du perl, ne serait-ce que 6 mois par exemple, vous perdez la connaissance que vous avez des bricolages possibles de Perl, et les détails commencent à vous échapper.

Perl est bien si vous en fait régulièrement, que vos programmes n'ont pas besoin d'une architecture orienté-objet (bien qu'il soit possible de faire de l'orienté-objet avec du Perl, mais c'est vraiment une horreur à écrire).

Perl, par toutes ses particularités et ses bricolages, a permis la constitution d'une communauté soudée, car assez élitiste finalement, Elite dans le sens de : qui maitrise tous les aspects de perl.

Je crois que je vais définitivement arrêter de coder en Perl mes programmes, et passer sur Python parce que je n'ai plus le temps de réapprendre les tortures mentales nécessaire à la programmation en Perl.