PHP 5.3 : Closures et Lambdas (partie 1)

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

Ceux d'entre-vous qui ont eu l'occasion de programmer en Javascript << moderne >> connaitront : PHP 5.3 introduit les notions de << lambdas >>, et de << closures >>.

Il y en a tant à dire sur ces deux nouveautés très liées que j'ai choisi de découper cet articles en deux parties : une que je publie aujourd'hui (lundi 03/11), et l'autre qui sera pour demain, mardi 4 novembre 2008.


Sommaire de cette première partie :


Définition et appel d'une fonction Lambda

Avant tout, une fonction << lambda >> est une fonction << anonyme >> : aucun nom ne lui est donné au moment de sa déclaration, contrairement à ce à quoi nous sommes habitués.

Fonction anonyme simple

Une fonction anonyme se défini, en PHP 5.3, de la manière suivante :

function () {
    echo "<p>Hello, World!</p>";
};

Notez dès maintenant l'importance du point-virgule à la fin de la définition de la fonction anonyme !
Sans lui, vous obtiendrez une Parse Error :

lambda-php-5.3-parse-error-si-oubli-point-virgule.png

Cette portion de code, où le nom de la fonction est absent, est interdite en PHP <= 5.2, et lèverait une Parse Error :

lambda-php-5.2-parse-error-1.png


Définir une fonction anonyme de la sorte n'est à première vu guère utile : nous ne pouvons pas l'appeler et exécuter la portion de code qu'elle contient !

Pour résoudre ce problème, stockons cette fonction anonyme au sein d'une variable, lors de sa déclaration :

$func = function () {
    echo "<p>Hello, World!</p>";
};

Nous pouvons maintenant appeler notre fonction, en appliquant l'opérateur () (qui reste l'opérateur d'appel de fonctions ^^) à notre variable :

$func();

Et le résultat obtenu est, en toute logique, l'affichage d'un Hello, World! !


Fonction anonyme avec paramètres

Bien évidemment, il est possible de déclarer une fonction anonyme comme attendant des paramètres ; la syntaxe est celle à laquelle on aurait logiquement tendance à s'attendre :

$func = function ($param) {
    echo "<p>Hello, $param!</p>";
};

Et pour l'appel :

$func('there');

Ce qui provoque l'affichage d'un autre message typique : Hello, there![1].

Et si nous avions appelé notre fonction lambda sans paramètre, nous aurions obtenu, comme pour une fonction << normale >>, l'affichage d'un warning :

lambda-php-5.3-warning-missing-argument.png

(Et de la notice qui va avec, la variable $param n'étant pas renseignée au sein de la fonction)

A noter : le message d'avertissement ne peut ici pas donner le nom de la fonction : il s'agit d'une fonction anonyme !

Je ne présente pas d'exemple ici, mais le passage de paramètre par référence est bien entendu possible, toujours en préfixant le paramètre d'un &.


Appel d'une fonction anonyme via call_user_func

Bien entendu, comme pour les autres fonctions que nous avons l'habitude de manipuler, il est possible d'utiliser call_user_func et assimilées pour appeler une fonction lambda.

Reprenons nos deux exemples de fonctions anonymes, l'une attendant un paramètre, et l'autre pas :

$func1 = function () {
    echo "<p>Hello, World!</p>";
};

$func2 = function ($param) {
    echo "<p>Hello, $param!</p>";
};

Ces fonctions peuvent être appelées à l'aide de call_user_func :

call_user_func($func1);

call_user_func($func2, 'You');

ou de call_user_func_array :

call_user_func_array($func1, array());

call_user_func_array($func2, array('You'));

Dans les deux cas, nous obtiendrons des affichages du même genre que ceux que nous obtenions déjà précédemment, à savoir :

Hello, World!

Hello, You!

En somme, les différentes manières d'appeler une fonction continuent à << fonctionner >> et sont applicables aux fonctions anonymes !


Oui, il existait déjà create_function... Mais !

Arrivés ici, certains d'entre vous se disent peut-être[2] qu'il était déjà possible de définir des fonctions anonymes depuis PHP 4.x, en utilisant la fonction create_function.

Effectivement, c'était déjà chose possible, en utilisant une syntaxe de la forme suivante pour la déclaration d'une fonction anonyme :

$func = create_function('$who', 'echo "<p>Hello, $who!</pre>";');

Et pour l'appel de la fonction, comme en PHP >= 5.3 avec les lambdas :

$func('World');

Mais autant create_function peut être utilisable pour générer des fonctions courtes et simples du genre de ce Hello, World!, autant ça devient l'enfer dès qu'il s'agit de mettre en place des fonctions anonymes un tant soit peu longues ou complexes : il faut penser à échapper les guillemets, les $, ... Et puisque l'ensemble de la fonction est défini au sein d'une chaine de caractère, vous ne bénéficiez d'aucune des fonctionnalités de votre IDE (auto-complétion, code-hint, coloration syntaxique, ...) !

En somme, je ne sais pas vous, mais je n'ai jamais apprécié create_function, alors que les fonctions anonymes de PHP 5.3 me semblent d'emblée plus alléchantes, ne serait-ce qu'en termes de syntaxe !


Et au niveau des performances, me direz-vous ?
Je ne ferai pas un bench complet ou approfondi -- si vous en faites un, postez un commentaire ! -- mais voici un premier test, hyper-minimaliste :

Définissons deux fonctions :

  • La première, pour tester la création et l'appel d'une fonction anonyme à l'aide de create_function,
  • et la seconde, en utilisant la nouvelle fonctionnalité de lambdas introduite en PHP 5.3 :
function test_create_function()
{
    $func = create_function('$a', 'echo $a;');
    $func('*');
} // test_create_function

function test_lambda()
{
    $func = function ($a) {
        echo $a;
    };
    $func('.');
} // test_lambda

Et appelons ces deux fonctions un nombre significatif de fois :

$nbIterations = 10000;
for ($i=0 ; $i<$nbIterations ; $i++) {
    test_lambda();
    test_create_function();
}

La sortie écran est comme attendue : une alternance de . et de * ; mais ce qui est plus intéressant pour nous est au niveau de ce que nous pouvons obtenir si nous profilons l'exécution de cette portion de script (profiling obtenu à l'aide de Xdebug, graphe d'appels obtenu via KCacheGrind (Qui ne fonctionne que sous Linux ; sous Windows, vous pouvez utiliser WinCacheGrind, mais il est moins riche en fonctionnalité : typiquement, il ne permet pas de tracer le graphe d'appels que je présente ici)) :

lambda-vs-create_function-bench-1.png

(L'exécution des deux fonctions anonymes n'apparait pas sur le graphique : elles durent si peu de temps que c'est négligeable -- et négligé par KCacheGrind ^^ ; au besoin, ajoutez une boucle de quelques milliers d'itérations pour consommer du temps CPU sauvagement, et vous les verrez apparaitre ^^ )

En somme, très grossièrement, sur un exemple volontairement simpliste, utiliser une fonction lambda de PHP 5.3 est de l'ordre de la dizaine de fois plus rapide que l'équivalent à base de create_function !

La raison (probable) ? Le fait que create_function compile la fonction à chaque fois qu'elle est re-créée, alors qu'en utilisant les fonctionnalités de PHP 5.3, elle n'est compilée qu'une seule fois, à la compilation du script ?
Si vous avez plus d'information à ce sujet, je suis intéressé !


Utiliser une fonction anonyme comme fonction de callback

Un des exemples évident d'utilisation d'une fonction anonyme est comme fonction de callback -- c'est d'ailleurs un des cas où j'ai le plus tendance à voir create_function utilisée.

Fonction de callback et array_map

Un premier exemple est l'utilisation de la fonction array_map, qui permet d'appeler une fonction sur chacun des éléments d'une liste.

Un exemple typique d'utilisation de array_map, en n'utilisant pas de fonction anonyme, serait le suivant :

function callback($item) {
    return ucwords($item);
}

$data = array('iron maiden', 'judas priest', 'rammstein');
$output = array_map('callback', $data);
var_dump($output);

Nous commençons par déclarer une fonction, et utilisons array_map pour l'appliquer à tous les éléments d'une liste.

Seulement, lorsque nous ne souhaitons utiliser cette fonction qu'en un seul endroit de notre code, il est dommage de devoir la déclarer, lui trouver un nom, et nous assurer qu'il est unique pour notre script, pour éviter les éventuels problèmes de redéfinitions de fonctions...
De plus, selon le contexte, nous ne pouvons pas toujours définir notre fonction à proximité de l'emplacement où nous l'appelons, ce qui ne facilite pas la compréhension du code...

Pour éviter ce genre de problème, nous utilisons parfois la fonction create_function pour créer une fonction anonyme << sur place >> :

$data = array('iron maiden', 'judas priest', 'rammstein');
$output = array_map(create_function('$item', 'return ucwords($item);'), $data);
var_dump($output);

Mais nous retombons sur les problèmes de create_function, que je citais plus haut ;-(

Arrive donc PHP 5.3 et sa nouvelle syntaxe permettant de déclarer des fonctions anonymes :

$data = array('iron maiden', 'judas priest', 'rammstein');
$output = array_map(function ($item) {
        return ucwords($item);
    },
    $data);
var_dump($output);

N'est-ce pas plus lisible ?
(Notez notamment la coloration syntaxique, qui fonctionne ici, et pas dans le cadre de l'exemple précédent)

Et pour chacune des trois syntaxes, le résultat obtenu est identique :

array
  0 => string 'Iron Maiden' (length=11)
  1 => string 'Judas Priest' (length=12)
  2 => string 'Rammstein' (length=9)

Pour chacun des éléments de notre liste, la première lettre de chaque mot a été mise en majuscule, via la fonction ucwords.

(Oui, pour un exemple aussi simple, nous aurions pu utiliser ucwords directement, sans l'encapsuler dans une fonction qui nous soit propre, je l'admet ; mais c'est l'idée qui compte)


Fonction de callback et array_walk

Juste pour la forme, voici un autre exemple, en utilisant cette fois-ci array_walk, qui attend une fonction de callback travaillant avec un paramètre passé par référence :

$data = array('iron maiden', 'judas priest', 'rammstein');
array_walk($data, function (& $item) {
        $item = ucwords($item);
    });
var_dump($data);

Le résultat obtenu sera exactement le même que juste au-dessus, à savoir :

array
  0 => string 'Iron Maiden' (length=11)
  1 => string 'Judas Priest' (length=12)
  2 => string 'Rammstein' (length=9)

A vous d'imaginer d'autres exemples d'utilisation ; je suis sûr que vous en trouverez, le moment venu %)

Ou plutôt, à vous de penser à cette nouvelle fonctionnalité au moment où vous en aurez besoin !


Fonctions anonymes et variables externes : use

PHP 5.3 introduit un nouveau mot-clef, dont le but est de permettre l'import de variables au sein d'une fonction anonyme : use.

Import de variable par copie

Utilisé pour les fonctions anonymes[3], le mot-clef use permet d'importer des variables externes, issues de l'espace de noms << global >>, au sein de la fonction lambda.

Illustrons ceci par un exemple :

$var = 'World';
$func1 = function () {
    echo "<p>Hello, $var!</p>";
};
$func2 = function () use ($var) {
    echo "<p>Hello, $var!</p>";
};

Que faisons-nous ici ?

  • Nous déclarons une variable nommée $var,
  • puis une première fonction anonyme, sur laquelle nous conserverons accès via la variable $func1,
  • et une seconde fonction anonyme, à laquelle nous pourrons accéder à l'aide de la variable $func2.
    • Pour cette fonction, nous déclarons l'import de la variable $var,
    • en faisant suivre la liste des paramètres de la fonction (qui n'en n'attend aucun, ici) du mot-clef use et de la liste des variables à importer, entre parenthèses.
    • Cela rendra la variable globale $var accessible depuis le corps de notre seconde fonction anonyme !

Et si, maintenant, nous essayons d'appeler nos deux fonctions anonymes :

$func1();   // Notice: Undefined variable: var in /home/php/php53/closures/lambda-5.3-use.php on line 6
            // => Hello, !
$func2();   // Hello, World!

En exécutant la première fonction, la variable $var n'est pas connue : elle a été déclarée en dehors de toute fonction, elle est donc globale, et n'est pas visible depuis le corps des fonctions de notre script -- ce point n'a pas changé par rapport aux versions précédentes de PHP.
L'appel de la première fonction entraine donc une notice, et l'affichage suivant :

lambda-php-5.3-no-use-notice.png

Par contre, lorsque nous exécutons l'appel de la seconde fonctions anonyme, nous obtenons l'affichage suivant :

Hello, World!

La variable $var valant "World" a été importée depuis l'espace de noms global, puisqu'elle a été déclarée dans la liste use de la fonction.

En somme, l'utilisation du mot-clef use lors de la définition d'une fonction entraine l'import d'une ou plusieurs variable(s) externe(s) à la fonction lambda -- en lecture seule (Faites l'essai : modifiez $var dans la fonction : cela n'aura pas d'impact sur sa valeur en dehors de celle-ci).
En quelque sorte, on pourrait penser à l'instruction global, que nous rencontrions parfois auparavant... Mais global ne répond pas aux besoins des closures, que nous présenterons demain, en seconde partie de cet article...


Import de variable par référence

L'import de variables au sein une fonction anonyme à l'aide du mot-clef use peut aussi se faire par référence, toujours en utilisant l'opérateur &, de manière à ce que la fonction lambda ait accès en << lecture/écriture >> aux variables souhaitées.

Pour illustrer ceci, un nouvel exemple, déclarant et appelant deux fonctions anonymes quasiment identiques :

$var = 0;

$func1 = function () use ($var) {
    echo "<p>Début 1 : $var</p>";
    $var = 1;
    echo "<p>Fin 1 : $var</p>";
};

$func2 = function () use (& $var) {
    echo "<p>Début 2 : $var</p>";
    $var = 2;
    echo "<p>Fin 2 : $var</p>";
};

Une seule différence est à noter :

  • Pour la première fonction, la variable $var est importée par copie,
  • alors que pour la seconde fonction, elle est importée par référence.

Appelons la première fonction anonyme :

echo "<p>Avant 1 : $var</p>";   // Avant 1 : 0
$func1();                       // Début 1 : 0
                                // Fin 1 : 1
echo "<p>Après 1 : $var</p>";   // Après 1 : 0

Nous obtenons quatre affichages :

  • Avant l'appel à la fonction, $var globale vaut 0.
  • En arrivant dans la fonction, $var locale à la fonction vaut 0, puisque c'est une copie locale de la variable $var globale.
  • A la fin de la fonction, $var locale à la fonction a été mise à 1.
  • Et une fois l'appel de la fonction terminé, on repasse à l'affichage de $var globale, qui n'a pas été modifiée : elle vaut toujours 0.

Au tour de la seconde fonction, à présent :

echo "<p>Avant 2 : $var</p>";   // Avant 2 : 0
$func2();                       // Début 2 : 0
                                // Fin 2 : 2
echo "<p>Après 2 : $var</p>";   // Après 2 : 2

Ici encore, nous obtenons quatre sorties écran :

  • Avant l'appel à la fonction, $var globale vaut 0.
  • En arrivant dans la fonction, $var a été importée par référence ; nous travaillerons donc avec $var globale, même au sein de la fonction -- et $var vaut actuellement 0.
  • A la fin de la fonction, $var (globale, puisqu'importée par référence) a été mise à 2.
  • Et une fois l'appel de la fonction terminé, cette modification a été conservée, puisque la fonction travaillait, par le biais de l'import par référence, sur $var globale : nous affichons donc encore 2.

Rien de très surprenant, donc :

  • use entraine l'import de variables au sein d'une fonction anonyme,
  • et on peut obtenir un import par référence, en utilisant l'opérateur &.


Fonction anonyme vue par le moteur PHP

A titre de curiosité, comment est-ce qu'une fonction anonyme est vue par le moteur PHP ?

Pour donner un premier élément de réponse à cette question, créons une fonction lambda, et appelons var_dump dessus :

$func = function ($param) {
    echo "<p>Hello, $param!</p>";
};

var_dump($func);

Le résultat obtenu est le suivant :

object(Closure)[1]

Pour PHP, une fonction anonyme -- une lambda -- est un objet : une instance de classe Closure...

...Voila qui marque la fin de la première partie de cet article, et annonce la seconde partie, que je publierai demain matin, qui traitera justement -- entre autres -- des closures !


Notes

[1] Pour ceux qui ne verraient pas à quoi je fais allusion, Hello there est une réplique de Obi Wan Kenobi, à la fois dans Star Wars Ep III - Revenge of the Sith, et dans Star Wars Ep IV - A New Hope

[2] J'anticipe les commentaires que j'aurais probablement eu si je n'avais pas parlé de create_function ^^

[3] Nous verrons d'ici quelques jours que use est aussi utilisé dans un autre contexte