PHP 5.3 : Closures et Lambdas (partie 2)

4 novembre 2008closure, php, php-5.3
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

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 $compteur est initialisée avec la valeur 10.
    • 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.
    • $a1 est donc un « pointeur » vers la fonction anonyme interne.
    • $compteur de valeur 10 continue d’exister, puisqu’elle est liée par la fonction anonyme sur laquelle pointe $a1.
  • 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-ci 50.
    • Cette variable de valeur 50 continue d’exister, puisqu’elle est liée par la fonction anonyme sur laquelle pointe $a2.

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 $a1 et $a2 a conservé la valeur qui lui avait été passée lors de sa création, via le mécanisme de closure.
  • Même si $compteur est 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-clef use s’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 :

appel-fonction-sur-objet-no-__invoke-fatal-error.png

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";
};

Pour manipuler cette fonction anonyme à l’aide de l’API de Reflection, nous devons utiliser la classe ReflectionFunction[2] :

$reflectedFunc = new ReflectionFunction($func);

Et si nous essayons d’afficher la définition de notre fonction anonyme :

echo '<pre>';
Reflection::export($reflectedFunc);
echo '</pre>';

nous obtenons :

Closure [  function {closure} ] {
  @@ C:\dev\tests\temp\temp.php 6 - 8

  - Bound Variables [1] {
      Variable #0 [ $c ]
  }

  - Parameters [2] {
    Parameter #0 [  $a ]
    Parameter #1 [  &$b ]
  }
}

Les fonctions anonymes peuvent donc elles aussi être manipulées via l’API de Reflection.


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 »

[2] Merci à Gérald et Sylvie pour avoir signalé que les fonctions anonymes se manipulaient avec ReflectionFunction, et non ReflectionMethod — c’est quelque chose qui a dû changer entre le moment où j’ai écrit cet article, et la version courante de PHP 5.3

Vous avez apprécié cet article ? Faites-le savoir !

Ce blog a récemment été migré vers un générateur de sites statiques et je n'ai pas encore eu le temps de remettre un mécanisme de commentaires en place.

Avec un peu de chance, je parviendrai à m'en occuper d'ici quelques semaines ;-)