PHP 5.3 : Closures et Lambdas (partie 2)
Par Pascal MARTIN le mardi 4 novembre 2008, 07:30 - Développement Web - Lien permanent
Les exemples correspondant à ce point se trouvent dans le répertoire "closures".
Voici la seconde partie de cet article traitant de l’ajout à PHP 5.3 des notions de « lambdas » et de « closures » — la première partie ayant été publiée hier matin.
Sommaire de cette seconde partie :
Création et utilisation de closures
Après avoir vu comment créer des fonctions anonymes et une partie des possibilités qu’elles offraient, voici venu le temps de nous intéresser de plus près aux closures !
Définir la notion de closure (parfois appelées « fermeture » en français, mais le terme n’est que très rarement employé) est difficile… En s’inspirant de Wikipedia - closure, on pourrait dire que :
Une closure est une fonction qui est évaluée dans un environnement contenant une ou plusieurs variables liées, auxquelles elle peut accéder au moment de son exécution.
Dans certains langages — dont Javascript, et PHP >= 5.3 — une closure peut exister lorsqu’une fonction est définie au sein d’une autre, et que la fonction interne fait référence à des variables de la fonction externe.
A l’exécution, une closure est formée : elle est constituée du code de la fonction interne et des références aux variables externes utilisées par celle-ci.
En PHP, une closure se construit de la manière suivante :
- Une variable locale est créée dans une première fonction "externe",
- Une seconde fonction "interne" est définie à l’intérieur de la première fonction, sous forme d’une fonction anonyme,
- Et cette fonction "interne" importe la variable locale de la fonction "externe", à l’aide du mot-clef
use.
Closure en lecture-seule
Par exemple, voici la mise en application de ces trois principes :
$func = function ($arg) { $compteur = $arg; // Copie privée, en lecture seule return function () use ($compteur) { return ++$compteur; }; };
A l’appel de la fonction pointée par $func, une closure est créée : la fonction interne référence la variable $compteur, et ce même lorsque que la fonction externe a terminé son exécution.
Puisque la fonction externe retourne une fonction anonyme, nous pouvons l’appeler, typiquement de la manière suivante :
$a1 = $func(10); $a2 = $func(50);
Nous venons ici de créer deux fonctions anonymes :
- Lors du premier appel, notre fonction "externe" est appelée, et sa variable locale
$compteurest initialisée avec la valeur10.- Cette variable est alors importée au sein de la fonction interne, qui est retournée par la fonction externe, et stockée dans la variable
$a1. $a1est donc un « pointeur » vers la fonction anonyme interne.$compteurde valeur10continue d’exister, puisqu’elle est liée par la fonction anonyme sur laquelle pointe$a1.
- Cette variable est alors importée au sein de la fonction interne, qui est retournée par la fonction externe, et stockée dans la variable
- De la même manière, lors du second appel, une seconde closure est créée, qui mène à la création en mémoire d’une seconde variable
$compteur, valant cette fois-ci50.- Cette variable de valeur
50continue d’exister, puisqu’elle est liée par la fonction anonyme sur laquelle pointe$a2.
- Cette variable de valeur
Pour illustrer la conservation en mémoire de ces deux variables par le mécanisme de fermetures, exécutons la portion de code suivante :
echo '<pre>'; echo 'a1 : ' . $a1() . "\n"; // 11 echo 'a2 : ' . $a2() . "\n"; // 51 echo 'a1 : ' . $a1() . "\n"; // 11 echo 'a2 : ' . $a2() . "\n"; // 51 echo 'a1 : ' . $a1() . "\n"; // 11 echo 'a2 : ' . $a2() . "\n"; // 51 echo '</pre>';
L’affichage obtenu est le suivant :
a1 : 11 a2 : 51 a1 : 11 a2 : 51 a1 : 11 a2 : 51
Deux choses sont à noter :
- Chacune des deux fonctions anonymes pointées par
$a1et$a2a conservé la valeur qui lui avait été passée lors de sa création, via le mécanisme de closure. - Même si
$compteurest modifiée au sein de la fonction "interne", sa valeur au sein de la fonction "externe" n’est pas modifiée : l’import effectué à l’aide du mot-clefuses’est fait en lecture-seule.
En somme, nous venons de créer une paire de fonctions anonymes utilisant le principe de fermeture pour conserver en mémoire des « variables privées ».
Closure en lecture-écriture
Voyons à présent comment faire pour que ces « variables privées » soient accessibles non plus en lecture-seule, mais aussi en écriture, afin que leur valeur puisse être modifiée par la fonction interne, et que ces modifications perdurent d’un appel à l’autre.
Nous avons vu dans la première partie de cet article que le mot-clef use permettait l’import d’une variable au sein d’une fonction anonyme, et qu’il fallait utiliser l’opérateur & pour obtenir un import en lecture-écriture et non en simple lecture.
C’est ce que nous faisons ici : par rapport à l’exemple précédent, nous ajoutons uniquement l’opérateur & dans la liste de variables importées via use :
$func = function ($arg) { $compteur = $arg; // Copie privée, en lecture / écriture return function () use (& $compteur) { return ++$compteur; }; };
Et comme précédemment, nous créons deux instances[1] de cette fonction anonyme :
$a1 = $func(10); $a2 = $func(50);
Et appelons quelques fois nos deux fonctions, accessibles par le biais de $a1 et $a2 :
echo '<pre>'; echo 'a1 : ' . $a1() . "\n"; // 11 echo 'a2 : ' . $a2() . "\n"; // 51 echo 'a1 : ' . $a1() . "\n"; // 12 echo 'a2 : ' . $a2() . "\n"; // 52 echo 'a1 : ' . $a1() . "\n"; // 13 echo 'a2 : ' . $a2() . "\n"; // 53 echo '</pre>';
L’affichage obtenu est à présent le suivant :
a1 : 11 a2 : 51 a1 : 12 a2 : 52 a1 : 13 a2 : 53
Ici encore, les deux fonctions anonymes pointées par $a1 et $a2 ont conservé la valeur qui leur avait été passée lors de leurs création, via le mécanisme de closure, mais, cette fois, les modifications apportées à $compteur au sein de la fonction "interne" ont été conservées par la fonction externe.
Ceci illustre le mécanisme de closure en lecture-écriture, et montre que la variable $compteur conservée en mémoire par la closure est celle de la fonction "externe".
Autrement dit, le mécanisme de closure permet de créer des variables au sein de la fonction "externe", qui conserveront leur valeur aussi longtemps que l’on aura conservé un pointeur sur la fonction "interne".
Ces variables seront accessibles par la fonction interne, éventuellement en écriture si nous avons utilisé & lors de leur import, tout en n’étant pas visibles du reste de notre script.
Finalement, pour ceux d’entre-vous qui programment fréquemment en Javascript (ou en Python, ou en scheme, ou en … ), pas grand chose de nouveau sous le soleil : la différence principale est qu’en PHP, les closures peuvent être en lecture seule.
En PHP, quel est l’intérêt des closures ? Quelle sera leur utilisation ?
Sachant que nous avons déjà à notre disposition les fonctionnalités objet de PHP, les closures seront probablement moins utilisées qu’en Javascript… Mais qui sait ? %)
Appel de fonction sur un objet
PHP 5.3 ajoute la possibilité d’utiliser la syntaxe d’un appel de fonction sur un objet, en lui appliquant l’opérateur ().
Méthode magique __invoke
Pour cela, une nouvelle méthode magique a été ajoutée : __invoke : lors d’un appel de fonction sur une instance de classe comportant une méthode __invoke, c’est cette méthode qui sera appelée.
Voici une classe d’exemple :
class MyString { public $str; public function __construct($a) { $this->str = $a; } // Appelée quand on appelle dynamiquement un // objet instance de cette classe public function __invoke($a) { var_dump(__METHOD__); $this->str = $a; } }
Le seul point « notable » ici est la présence de la méthode magique __invoke.
Une fois cette classe définie, commençons à l’utiliser, en l’instanciant :
$str1 = new MyString('1111'); var_dump($str1);
L’affichage obtenu en sortie est, comme nous pourrions nous y attendre, le suivant :
object(MyString)[1] public 'str' => string '1111' (length=4)
Maintenant, effectuons un appel de fonction sur notre objet :
$str1('2222'); var_dump($str1);
Nous obtiendrons alors l’affichage suivant :
string 'MyString::__invoke' (length=18) object(MyString)[1] public 'str' => string '2222' (length=4)
En somme, un appel de fonction sur un objet passe par la nouvelle méthode magique __invoke.
Appel de fonction sur un objet d’une classe ne définissant pas __invoke : Fatal Error !
A noter tout de même : l’appel de fonction sur un objet n’est possible que pour les classes définissant une méthode __invoke : si cette méthode n’existe pas, l’appel de fonction sur un objet de la classe n’est pas possible, et mène à une Fatal Error.
Par exemple, utilisons la portion de code suivante :
class MyString { public $str; public function __construct($a) { $this->str = $a; } } $str1 = new MyString('1111'); var_dump($str1); // Fatal error: Function name must be a string // (l'objet n'a pas de méthode __invoke) $str1('2222'); var_dump($str1);
Et nous obtiendrons l’affichage suivant :
Avec la Fatal Error « qui va bien », donc : sans méthode __invoke au niveau de la classe, PHP n’essaye même pas de travailler avec un objet, et considère uniquement que vous vouliez appeler une fonction dont le nom aurait été contenu dans $str1, alors considérée comme une variable de type chaine de caractères — ce qu’elle n’est pas.
Reflection
Qui dit modifications au niveau des fonctions et des classes dit modifications et nouveautés au niveau de l’API de Reflection permettant de les manipuler !
Reflection d’une fonction anonyme
Commençons par déclarer une fonction anonyme, comme vu maintes fois au cours de cet article :
$c = 'World'; $func = function ($a, & $b) use ($c) { echo "Hello, $c!\n"; };
$func n’étant pas un nom de fonction, mais, en interne, un objet, nous ne pouvons pas utiliser ReflectionFunction : pour manipuler une fonction anonyme, nous devons utiliser ReflectionMethod :
$methode = new ReflectionMethod($func);
Et si nous essayons d’afficher la définition de notre fonction anonyme :
echo '<pre>'; Reflection::export($methode); echo '</pre>';
nous obtenons :
Method [ public method __invoke ] {
- Parameters [2] {
Parameter #0 [ $a ]
Parameter #1 [ &$b ]
}
}
A noter : notre fonction anonyme, en interne, est un objet, nous l’avions vu plus haut…
… Et l’exécution d’une fonction anonyme passe par la méthode __invoke de cet objet !
Les fonctions anonymes peuvent donc être manipulées via l’API de Reflection… A vous de voir quels peuvent être vos besoins, mais, à titre d’exemple, voici comment appeler notre fonction à l’aide de l’API de Reflection :
// Pour appeler la méthode, passons par sa "closure" : $closure = $methode->getClosure($func); $a = 20; $closure(10, $a);
Je manque un peu de "cas réels" à montrer… Si vous utilisez régulièrement l’API de Reflection — en particulier dans ce contexte —, je suis preneur d’exemples concrets ^^
Appel d’une méthode de classe via getClosure
Supposons maintenant que nous ayons défini la classe suivante :
class ClassA { public $var = 'World'; public function a() { echo "Hello, {$this->var}!\n"; } }
Nous pouvons accéder à la méthode a de notre classe en utilisant l’API de Reflection :
$classe = new ReflectionClass('ClassA'); $methode = $classe->getMethod('a'); echo '<pre>'; Reflection::export($methode); echo '</pre>';
Ce qui nous donne l’affichage suivant :
Method [ public method a ] {
@@ /home/php/php53/closures/reflection-5.3-2.php 7 - 9
}
Et pour invoker notre méthode, avant PHP 5.3, nous passions par une syntaxe de ce genre :
echo '<pre>'; $methode->invoke(new ClassA()); echo '</pre>';
A partir de PHP 5.3, avec l’ajout de la notion de closures, nous pouvons utiliser la syntaxe suivante :
echo '<pre>'; $objet = new ClassA(); $closure = $methode->getClosure($objet); $closure(); $objet->var = 'You'; $closure(); echo '</pre>';
Nous commençons par obtenir un accès à la méthode qui nous intéresse, à l’aide de getClosure, et, une fois que nous avons cet accès, nous pouvons appeler notre méthode directement, via la fonction anonyme pointée par $closure.
Et l’affichage obtenu correspond bien à celui attendu :
Hello, World! Hello, You!
Appel de méthodes privées via getClosure
Le principe que nous venons de voir est plus global : il permet notamment d’appeler des méthodes privées, qui ne seraient normalement pas accessibles depuis l’extérieur d’une classe :
class ClassA { protected $var = 'World'; private function a() { echo "Hello, {$this->var}!\n"; } } echo '<pre>'; $classe = new ReflectionClass('ClassA'); $methode = $classe->getMethod('a'); Reflection::export($methode); $objet = new ClassA(); $closure = $methode->getClosure($objet); $closure(); echo '</pre>';
Nous commençons par définir une classe contenant une méthode privée, que nous ne pouvons pas appeler directement, puis, à l’aide de l’API de Reflection et de sa nouvelle méthode getClosure, nous obtenons un pointeur sur cette méthode…
Et à partir de là, nous sommes en mesure de l’appeler, ce qui nous donne l’affichage suivant :
Method [ private method a ] {
@@ /home/php/php53/closures/reflection-5.3-3.php 7 - 9
}
Hello, World!
Bien évidemment, si le développeur d’une classe a choisi de déclarer des méthodes comme privées, ce n’est certainement pas par pur plaisir, mais plutôt parce qu’elles ne sont théoriquement pas utiles depuis l’extérieur de ladite classe… Et puisque personne d’autre que les méthodes de cette classe ne peut théoriquement les appeler, leur implémentation — et même leur existence — peut changer à tout moment !
Vous ne devriez donc jamais utiliser cette fonctionnalité au sein de vos applications…
…Mais cette possibilité peut être intéressante lors de la mise en place et du développement de tests unitaires automatisés sur une classe : il peut parfois être pratique de pouvoir tester unitairement certaines méthodes, même si elles sont privées ou protégées !
Pour terminer
Pour terminer cet article en deux parties, juste un lien : celui vers la RFC décrivant les fonctions anonymes et les closures en PHP.
Encore une fois, PHP 5.3 nous apporte une nouvelle fonctionnalité ; et encore une fois, j’ai hâte de voir comment elle sera utilisée…
… Qu’en pensez-vous ?
Notes
[1] Comme indiquée en fin de première partie, une fonction anonyme est un objet, une instance de la classe Closure. Il ne me semble donc pas choquant de parler « d’instance de fonction anonyme »
Pour être averti lors de la publication de nouvelles entrées, n'hésitez pas à vous abonner au flux RSS ou ATOM des articles de mon blog !

Commentaires
Ton article est interressant, mais je trouve qu'il manque vraiment des cas concrets où l'utilisation des closures est pertinente, voire nécessaire. Tu n'explique finalement pas vraiment à quoi ça pourrait servir.
Alors je me permet d'apporter une précision. Par exemple, on utilisera une closure et fonction anonyme quand on a besoin de passer une fonction en argument à une autre (typiquement une fonction de callback), et que dans cette fonction de callback on ait besoin d'accéder à des variables externes mais qu'on ne peut faire passer en argument à cette fonction de callback.
En fait, on peut en PHP <5.2 faire des sortes de closures, grâce au mot clé global. Ça revient au même, à la seule différence qu'avec une vrai closure, on peut accéder à des variables non globales, et qui sont accessibles que dans le scope parent. En php <5.2, les closures obligent à avoir en global toutes les variables que l'on veut utiliser (et on ne peut pas définir des variables de closure en lecture seule)
À part ça, j'ai du mal à voir la relation entre cette méthode magique __invoke et les closures/fonctions anonymes. Peux-tu apporter des précisions ? Y a-t-il vraiment un rapport ?
PS: j'ai souvent entendu le mot "cloture" en français comme traduction du mot closure.
Hello,
Non, je n'explique pas vraiment à quoi ça pourrait servir... parce que je ne les ai pas assez utilisées en PHP pour vraiment en avoir une idée clairement définie : globalement, comme c'est le cas pour probablement quasi tout le monde, je n'ai utilisé PHP 5.3 que pour tester, pour m'amuser... Et jamais encore pour une vraie application -- et il en sera ainsi pour encore des mois, voire des années, je suppose...
Donc je préfère donner déjà quelques exemples de "comment faire" ; et que les exemples d'utilisation viennent par eux-même, au fil des prochains mois, basés sur des cas 100% réels
Merci pour ton exemple, en tout cas
Pour ce qui est du lien entre Closures, fonctions anonymes, et
__invoke:- Une fonction anonyme est une instance de classe
- Appeler une fonction anonyme revient donc à effectuer un appel de fonction sur un objet -- Cf http://blog.pascal-martin.fr/post/php-5.3-2-closures-et-lambdas#appel-fonction-objet
- Un appel de fonction sur un objet passe par sa méthode
Il y a donc de fortes chances que l'appel d'une fonction anonyme passe par la méthodeClosure-- Cf http://blog.pascal-martin.fr/post/php-5.3-1-closures-et-lambdas#fonction-anonyme-interne__invoke__invokede la classeClosure.(Ou qu'il y ait un hack de ce style qui ait été mis en place -- je ne suis pas rentré dans les détails de l'implémentation)
PS : j'ai à peu près toujours utilisé , effectivement... Mais il est parfois pratique d'avoir un pseudo "synonyme" ^^
Ok, merci, je comprend mieux pour ce __invoke.
"Closure" c'est simplement "fermeture" bien que ça sonne étrangement, comme beaucoup de néologismes de l'informatique provenant de l'anglais. "Callback" c'est déjà plus utilisable : "fonction de rappel", ça sonne très bien alors abusons en plutôt que de souiller le français
@Laurentj : il n'y a pas de fermetures avant PHP 5.3, ne mêlons pas ruses et concepts
Le concept de "cadres" (on n'est pas obligé d'utiliser "scope" ^^), avec les fermetures, prend toute son ampleur ! Auparavant on pouvait seulement "enfermer" des variables dans des blocs délimités par des accolades si je ne me trompe pas - mais le besoin, s'il existe, est très rare je pense...