PHP 5.3 : Closures et Lambdas (partie 2)

le - Lien permanent 8 commentaires

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 !

Commentaires

1. Par Laurentj le 2008-11-04 10:55
Laurentj

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.

2. Par Pascal MARTIN le 2008-11-04 20:42
Pascal MARTIN

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 :

Il y a donc de fortes chances que l'appel d'une fonction anonyme passe par la méthode __invoke de la classe Closure.
(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é closure, effectivement... Mais il est parfois pratique d'avoir un pseudo "synonyme" ^^

3. Par Laurentj le 2008-11-05 10:37
Laurentj

Ok, merci, je comprend mieux pour ce __invoke.

4. Par Oscar Hiboux le 2010-02-05 04:42
Oscar Hiboux

"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...

5. Par Gérald le 2011-01-26 10:42
Gérald

Errata sur la partie Reflection, on doit utiliser ReflectionFunction et non ReflectionMethod pour inspecter une fonction anonyme.

=> http://fr2.php.net/manual/en/reflec...

6. Par alexouin le 2011-02-16 07:21
alexouin

Pascal ton blog est très intéressant, ça fait plusieurs fois que je le remarque.
Une question me taraude, as-tu quelque chose à voir avec le Pascal Martin qui avait fondé le club Pilotes virtuels de France il y a quinzaine d'années ?

7. Par Sylvie le 2011-05-22 19:22
Sylvie

Je confirme ce que dit Gérald, il y a bien une erreur, ReflexionMethod ne fonctionne pas pour les fonctions anonymes, il faut utiliser ReflexionFunction :
$methode = new ReflectionMethod($func);
$fonction = new ReflectionFunction($func);

8. Par Pascal MARTIN le 2011-05-27 19:07
Pascal MARTIN

Avec un peu de retard, désolé...


@Gérald et @Sylvie : effectivement...

Je suppose que c'est quelque chose qui a du changer entre le moment où j'ai fait mes tests et écrit cet article, et la version stable de PHP 5.3.

Je viens d'éditer la section concernée, pour supprimer les informations qui n'étaient pas valide ; et, avec un peu de chance, mettre quelque chose qui l'est plus (j'ai testé rapidement, mais sans plus).
Merci pour le signalement !

@alexouin : pas du tout.

Ce post n'est pas ouvert aux nouveaux commentaires (probablement parce qu'il a été publié il y a longtemps).