Passer des paramètres à un délégué grâce aux closures (fermetures)

Dans certaines fonctions « fermées » [1] définies au sein de diverses librairies, on peut passer en paramètre un délégué (ce qui correspond dans la plupart des langages à un pointeur sur fonction), afin de donner à cette première fonction la main sur l’exécution de la fonction pointée.

C’est le cas des fonctions setInterval() ou setTimeout() en javascript. La fonction setInteval() sera choisi ici à titre d’exemple. setInterval() attends un premier paramètre qui est une fonction à exécuter et un second paramètre qui détermine l’intervalle de temps en millisecondes entre chaque appel de cette fonction.

function onFrame()
{
    console.log('on frame');
}

function Play()
{
    setInterval(onFrame, 1000);
}

Play();

La fonction setInterval a donc la main sur la fonction passée en paramètre (ici onFrame) et l’exécutera quand bon lui semblera. Dans l’exemple ci-dessus la fonction déléguée onFrame a été externalisée par apport à la fonction Play.

Le problème

Il n’est pas possible de passer directement au délégué un ensemble de paramètres qui permettraient de modifier le comportement de la fonction pointée. En reprenant l’exemple précédent, il serait intéressant de pouvoir modifier la fonction onFrame() en onFrame(text) afin de pouvoir choisir le texte à afficher dans la console en lui passant un paramètre supplémentaire.

La solution

Les fermetures, en anglais closures, vont nous permettre de résoudre ce problème. Une fermeture est une fonction qui capture des références à des variables libres dans l’environnement lexical. C’est à dire que cette fonction utilise une ou plusieurs variables internes à son environnement mais déclarées hors de sa définition.

Exemple :

function parentFunction()
{
    var freeVariable = "Im free";
    
    var fonctionInterne = function(str)
    {
        return str + " : " + freeVariable;
    };
    
    return fonctionInterne;
};

var f = parentFunction();

console.log(f("Hello")); //Affiche "Hello : Im free"
  • L’environnement lexical est parentFunction où a été défini la variable freeVariable.
  • La fonction imbriquée fonctionInterne utilise la variable freeVariable qui se situe à l’extérieur de son corps – la variable freeVariable a été capturée.
  • La référence vers freeVariable est capturée par la fonction imbriquée fonctionInterne : freeVariable est donc une variable libre, tandis que fonctionInterne est une fermeture.

1. Définition de fermeture par des fonctions imbriquées

Pour ce faire, il faut implémenter une fonction qui en construit une autre. Ainsi les paramètres à passer se trouverons au niveau de la fonction parente et c’est celle-ci qui va construire la fonction enfant utilisant ces paramètres.

Ce qui donne :

function onFrame(text)
{
    return function()
    {
        console.log(text);
    }
}

function Play(text)
{
    setInterval(onFrame(text), 1000);
}


Play('My text');

Dans l’exemple ci-dessus, on voit que l’appel onFrame(text) va créer et retourner la fonction anonyme englobée par onFrame en remplaçant la variable text par la valeur de la variable passée en paramètre.

2. Définition de fermeture par des fonctions anonymes directement

La deuxième solution, assez connue et sûrement beaucoup plus utilisée, consiste tout simplement à déclarer une fonction anonyme directement passée à setInterval :

function Play(text)
{
    setInterval(function() {
        console.log(text);
    }, 1000);
}


Play('My text');

Cette solution a l’avantage de réduire le nombre de ligne de code. Elle peut être envisagée, si et seulement si, un comportement identique ne doit pas être utilisée ailleurs, auquel cas il y aurait une redondance de code. Il pourra aussi être avantageux d’externaliser cette fonction pour des raisons de lisibilité.

Conditions nécessaires

Il faut que le langage utilisé permette la définition de fermetures et le retour de fonction.

En savoir plus

Application partielle

En fait cette solution utilise un mécanisme bien connu en programmation fonctionnelle qui se nomme l’application partielle. Celui-ci consiste en l’implémentation d’une fonction qui a pour but de créer et retourner une fonction dont l’arité (le nombre de paramètres de cette fonction) est inférieure à sa propre arité. En d’autre mots, la fonction construite et retournée doit avoir moins de paramètres que sa fonction parente (la fonction constructrice).

Variables libres et capture

Dans certains langages de programmation, la capture d’une variable libre doit être explicitement spécifié. C’est par exemple le cas en C / C++ ou bien de PHP.

function runAction($action)
{
	echo "Action begin\r\n";
	$action();
	echo "Action end";
}

function main()
{
	$freeVariable = "Hello";
	runAction(function() use($freeVariable)
	{
		echo "My captured var's value : $freeVariable \r\n";	
	});
}

/* Affiche : 
---------------------------------------------
Action begin
My captured var's value : Hello 
Action end
---------------------------------------------
*/

main();

Le mot-clé use permet ici de capturer les variables fournies en paramètres.


Créer des fermetures peut servir dans de nombreux cas (souvent pour la gestion des événements) et permet de résoudre d’autres problèmes en allant jusqu’à la refactorisation du code. Malgré le grand intérêt que porte ce concept, il faut toujours l’utiliser avec parcimonie et avec soin car le mécanisme de création de fermetures est légèrement coûteux en temps et en espace et peux entrainer des fuites de mémoires.


 Notes

[1] Je distingue les fonctions sur lequel l’utilisateur à le contrôle aux fonctions déjà définies au sein d’une librairie ou d’un framework. Pour les premières que je qualifie comme ouvertes et modifiables, il s’agit des siennes où de celle de son équipe qu’il peut modifier. Pour les secondes que je qualifie comme fermés ou finies, il s’agit de celles qui sont difficilement modifiables sans provoquer des erreurs du fait de leur utilisation potentielle par de nombreux projets dépendants. Pour ces dernières, il ne faut pas confondre dans le cas présent avec du code source fermé opposé à l’open-source car inaccessible et non modifiable même si ce cas est contenu dans l’autre.

Laisser un commentaire

%d blogueurs aiment cette page :