Thu Jun 03 2010

Un point sur les Traits de PHP

et peut être même plus si affinité

Filed under PHP, Programming. 7 comments

Dernièrement, les Traits ont fait leur entrée dans le trunk de PHP. Je ne vais pas les présenter dans leur intégralité puisque la RFC correspondante dont les premières esquisses ont plus de deux ans, est suffisamment claire et détaillée.

Cependant, le document n'est pas très explicite en ce qui concerne l'utilisation d'interfaces et de traits au sein d'une même classe, ainsi que le statut des variables membres et leur accessibilité dans les méthodes des traits. Nous allons voir ce qu'il en est et les conséquences que cela implique.

Avant de commencer, il est vivement recommandé d'avoir lu la RFC. Juste la première partie qui concerne les traits. La seconde partie présente les Grafts que nous n'aborderons pas ici [1].

L'opération de flattening, aka le lissage

L'ordre dans lequel les traits sont manipulés par php est le suivant:

  • Un fichier source est chargé, et comme par hasard, celui-ci contient des traits.
  • Le parser rencontre une déclaration de trait. Ce dernier est pris en compte comme n'importe quelle classe [2]. C'est à dire qu'une structure zend_class_entry est crée et complétée. Seul un flag spécial précise qu'il s'agit d'un trait.
  • Le même parser rencontre une classe qui utilise un trait via le mot clé use ou un trait qui dépend d'un autre trait (on dit qu'il s'agit d'un trait composite). A la fin de la déclaration de la classe, que l'on appelle une client class ou host class, les opérations s'enchainent ainsi:
    • Les méthodes provenant des traits sont triées selon les directives as et insteadof. Une nouvelle table temporaire est construite accueillant ces méthodes candidates à la future intégration dans la classe. Certaines méthodes peuvent être ignorées s'il y a des conflits. On se chope alors un "Warning: Trait method blah has not been applied, because there are collisions with other trait methods".
    • L'opération de flattening (lissage) commence. Pour chaque méthode candidate qui n'est pas redéfinie (overridden) dans la classe client:
      • Vérification du prototype (la signature) par rapport aux éventuelles méthodes des classes parents. S'il y a des différences, et selon la configuration, c'est là qu'on reçoit un "Strict Standards: Declaration of Machin::truc() should be compatible with..".
      • La méthode est dupliquée (son zend_op_array est littéralement copié comme le fait l'extension runkit) et est ajoutée à la table des méthodes de la classe client. Certaines opérations irréversibles se produisent, notamment le fait que la méthode perd toute notion et tout lien avec son trait d'origine [3].
    • Vérification de la bonne conformance de la classe (maintenant dotée de nouvelles méthodes provenant des traits) par rapport aux méthodes abstraites ainsi qu'aux interfaces sensées être implémentées.
  • Le parser continue son job jusqu'à la fin du fichier.

OK, voila une bonne chose de faite.

Sachant que les méthodes des traits sont injectées pratiquement "les yeux fermés", tout se passe comme si celles-ci étaient définies naturellement dans la classe client. Les accès à $this se font exactement de la même manière, rien ne change.

On peut dès lors s'interroger sur certains idiomes qui ne devraient pas tarder à fleurir sur les coins de tables en cette période pré-estivale. En effet, la nature stateless des traits tels qu'implémentés à l'heure actuelle dans php n'est pas sans conséquence quant à l'organisation du code.

L'état des traits

En fait, ils n'en ont pas. On dit qu'ils sont stateless. C'est un choix délibéré et argumenté dans les documents académiques du Software Composition Group (SCG) sur lesquels l'implémentation de php est basée.

Les traits sont uniquement fonctionnels. Ils ne sont pas liés à un quelconque état qui leur serait propre, comme cela peut être le cas dans de l'héritage multiple. Par état, il faut comprendre variables membres. Seule la classe client peut en fournir.

Or, il y a fort à parier que des traits ne manipulant aucun état se feront rares.

Donc, que faire ?

Je vais mettre de coté l'utilisation de variables globales, statiques ou de Singletons. On veut juste faire en sorte que les traits puissent utiliser les variables membres de la classe client. Il n'y a pas 50 possibilités, il y en a juste 2:

  • Accès direct aux variables via $this->
  • Recours à des getters/setters fournis par la classe client

Quoique l'on fasse, ou que je sois, rien ne t'efface, je pense à toi, le trait est dépendant de sa classe client. Accéder directement à une variable avec $this->var peut (pourrait, pourra, pouvait, a pu?) sembler briser l'encapsulation, et nous reviendrons sur ce point. Utiliser des getters et setters est la méthode conseillée par le SCG en particulier parce que leur démonstration et implémentation est basée sur le langage Smalltalk et que de toute manière ils ne peuvent faire autrement.

OK, OK. Il est grand temps qu'on balance un code illustratif car toute cette prose commence à devenir lourdingue ;)

trait T1
{
  public function doSomething() {
    $this->var = 42;
    echo $this->var;
  }
}

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
}

class C
{
  use T1, T2;
  
  private $var;
  
  private function getVar() {
    return $this->var;
  }
  private function setVar($value) {
    $this->var = $value;
  }
}

$c = new C;
$c->doSomething();
$c->doSomethingElseButDoItWell(); // please

var_dump($c);

Cela donne, comme attendu:

4243object(C)#1 (1) { ["var":"C":private]=> int(43) }

Dépendances et requirements

La dépendance classe -> trait est simplement exprimée dans le code via le mot clé use. Celle-ci est vérifiée dès la compilation. La dépendance trait -> classe n'est quant à elle pas exprimée du tout dans le code précédent, en tout cas pas avant l'exécution. T1 dépend de la présence de la propriété $var et T2 dépend des getter et setter getVar() et setVar().

La RFC préconise l'usage de méthodes abstraites au sein des traits pour exprimer et vérifier à la compilation cette dépendance trait -> classe. Le document parle de requirements, ce qui est la même chose.

Réécrivons T2 en le bardant de méthodes abstraites afin de se conformer aux bonnes pratiques au paragraphe du dessus:

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
  
  /*private*/ abstract function getVar(); 
  /*private*/ abstract function setVar($value);
}

Ce frêle bout de code est en fait plus profond que ce que l'on pourrait imaginer à première vue, surtout quand on sait que ça compile et que ça fonctionne.

Premièrement, bien que les getters/setters de la classe client soient de visibilité private, cela ne pose pas de problème de les déclarer public dans le trait. La règle des méthodes abstract dit pourtant que la méthode concrète doit avoir une visibilité identique ou moins restrictive que la déclaration abstraite.

Cela fonctionne parce que l'opération de lissage décrite plus haut ne concerne que les méthodes des traits qui ne sont pas redéfinies dans la classe client. Cette redéfinition ne se fait que sur le nom de la méthode. La signature n'intervient pas du tout. La méthode qui porte le même nom dans la classe va complètement masquer celle du trait. Il est donc tout à fait possible de remplacer setVar($value) dans le trait par ceci:

/*private*/ abstract function setVar($value, array $bonus);

On pourrait s'attendre à un beau "Fatal error: Declaration of C::setVar() must be compatible with that of T2::setVar()" car les signatures sont clairement différentes. Et bien non. La phase de lissage rend possible ce comportement.

Exprimer les requirements d'un trait par l'intermédiaire de méthodes abstraites n'est donc que partiel. Seul le nom de la fonction compte. La signature et les éventuels type hintings sont ignorés.

Mais alors pourquoi ça fonctionne quand même? C'est simple, toutes les méthodes abstraites du trait qui ne sont pas masquées par celles de la classe client se retrouvent dans cette même classe toujours avec le statut abstract. Là php peut détecter la non implémentation de ces méthodes et ainsi s'opère la vérification partielle des dépendances trait -> class.

Deuxièmement (le premièrement est 4 ou 5 paragraphes plus haut;) pourquoi ai-je supprimé la visibilité private pour les méthodes abstraites du trait? Après tout, les getter et setter sont privés dans la classe C.

C'est une bien sage question.

La réponse est que l'on veut éviter cet effet indésirable: "Fatal error: Abstract function T2::getVar() cannot be declared private". C'est une conséquence du fait que les traits sont traités (c'est le cas de le dire) par php comme de simples classes lors de la compilation. Ce point fut brièvement abordé plus haut ici. Or, une méthode abstraite ne peut être déclarée private car php considère que celle-ci n'est pas accessible depuis des classes dérivées et qu'il est donc impossible d'y apporter une implémentation. Quelque part, ça a du sens...

De l'existentialisme

Jusqu'ici, nos getters et setters font leur job. On n'en demande pas plus. Pourtant, un sérieux problème se profile à l'horizon.

C::getVar() retourne une copie de la variable membre $var. Sa valeur n'est pas systématiquement dupliquée, php faisant du copy-on-write, mais nous restons dans une sémantique de copie. Toute modification opérée sur ce que retourne getVar() n'aura pas de répercussion sur la véritable variable membre. Cela peut être tout à fait justifié et suffisant.

Cependant, prenons un peu de recul. Nous avons affaire à un getter privé dont l'appel se fait d'une méthode pleinement intégrée à la classe et qui pourrait tout à fait être privée elle aussi. Nous évoluons dans un no man's land à la frontière de l'encapsulation, l'indirection et l'abstraction. Le fameux triptyque des mots en ion.

Le trait pourrait avoir besoins de manipuler autre chose qu'un simple scalaire, par exemple un array. Va-t-il falloir définir toujours plus de setters pour toujours plus d'opérations telles que l'ajout, l'insertion ou le retrait d'éléments? Faut-il gonfler la classe client en méthodes annexes à faible valeur ajoutée, annulant au passage les bénéfices en copy'n'paste sensés provenir de l'utilisation même du trait?

Une solution à mi-chemin entre rien du tout, c'est à dire accéder directement à la variable membre via $this->var, et une belle série de getters/setters serait de recourir à un simple accesseur retournant une référence sur la variable. Coté trait, l'affectation classique = ou par référence =& permet de controller la nature de la liaison suivant l'opération que l'on désire effectuer, ce qui laisse à php la possibilité de faire son job routinier de passage (retour) par copie.

Il ne reste plus qu'à dégainer les variables temporaires. Oui, car comme chacun sait, php est très frileux quand il s'agit de déréférencer automatiquement les retours de fonctions. Vous aurez beau bidouiller en ajoutant des parenthèses ou des accolades, seule la dernière ligne n'est pas une erreur dans les constructions suivantes:

$this->func()[]
$this->func()()
++$this->func()
$this->func() = 
$this->func()->chain()

C'est navrant, et c'est la syntaxe qui n'est pas bonne, le problème détecté étant un parse error dans la plupart des cas. Pour contourner ce problème, il faudra d'abord récupérer le retour de la fonction dans une variable puis travailler à partir de celle-ci.

Traits et Interfaces

Nous allons clore cet article par un exemple qui réunit tout ce qui a été vu: faire endosser à un trait l'implémentation d'une interface héritée de la classe client. Cette construction est à mon sens potentiellement utile pour les auteurs de libraires et frameworks.

Une telle classe client peut être utilisée dans un contexte polymorphique ou de type hinting sans reposer pour autant sur une hiérarchie parfois bancale de classes abstraites, ni sur la présence d'adaptateurs spécifiques et verbeux dans le cadre d'une composition ou aggrégation.

On choisit d'implémenter ArrayAccess qui est automatiquement disponible dans php car fournie par la SPL.

/*
interface ArrayAccess
{
  public function offsetGet($offset);
  public function offsetSet($offset, $value);
  public function offsetExists($offset);
  public function offsetUnset($offset);
} */

trait ArrayAccessImpl
{
  abstract function &data();
  
  public function offsetGet($offset) {
    $d = $this->data();
    return $d[$offset];
  }
  
  public function offsetSet($offset, $value) {
    $d = &$this->data();
    $offset === null ? $d[] = $value : $d[$offset] = $value;
  }
  
  public function offsetExists($offset) {
    return array_key_exists($offset, $this->data());
  }
  
  public function offsetUnset($offset) {
    $d = &$this->data();
    unset($d[$offset]);
  }
}

class MyClass implements ArrayAccess
                     //, Iterator // laissé en exercice
{
  private $data = array(
    'a' => 3.14,
    'b' => null,
    'c' => true,
  );
  
  use ArrayAccessImpl;
  
  private function &data() {
    return $this->data;
  }
}

$c = new MyClass;

var_dump(isset($c['b']));
$c[] = $c['a'];
unset($c['c']);
var_dump($c);

Note: l'utilité plus que douteuse de MyClass n'est pas fortuite, il s'agit juste d'un code d'exemple (ma spécialité;)

Conclusions

Comme d'hab, RAS?

Traits're in da trunk, mais cela ne veut pas dire pour autant qu'ils seront dans le futur php, ni sous leur forme actuelle. Cependant, vu l'avancement du projet, le contraire serait étonnant.

Si tout se passe comme prévu, alors j'ai hâte de voir comment ils seront employés à l'avenir. Il s'agit d'un réel nouveau paradigme pour composer du code OOP. Par exemple, pour valider la pertinence des traits, le SCG a réimplémenté toute la hiérarchie des Collections de Smalltalk.

Je salue mageekguy pour son billet Et si on tirait des traits ? qui est à l'origine de mon soudain regain d'intérêt.

 

[1] Le statut des grafts est bien plus hypothétique puisqu'aucune implémentation n'existe à l'heure actuelle.

[2] Un trait peut déclarer des variables membres ou dériver d'une classe. Cela n'est pas interdit (pour l'instant?). Il ne peut cependant pas implémenter une interface. Cela résulte en une fatal error.

[3] Les éventuelles variables membres déclarées dans le trait auquel la méthode appartenait sont purement et simplement ignorées.

Tags php, traits

Comments

  1. mageekguy, Thu Jun 03 2010

    La première partie est tout simplement excellente.

    Un peu plus de référence sur les endroits d'ou extraire le code C concerné et cela aurait été parfait (oui, je suis fainéant, c'est le propre de tout bon informaticien ;)).

    A quand le billet "Comprendre le Zend Engine (ou le C Macro) en une leçon" ?

  2. Ivan Enderlin, Thu Jun 03 2010

    J'attends les traits dans PHP depuis plusieurs mois et ça pourrait considérablement améliorer le code dans les bibliothèques (notamment sur des gestions de flux où on fait beaucoup de copier/coller de code -- déplorable --).

  3. metagoto, Thu Jun 03 2010

    @mageekguy

    La première partie, elle va jusqu'où ? ;)

    Si l'exercice a plu, je pourrai tenter de le réitérer sur un autre sujet qui me tient à coeur, à savoir les nouveautés qui se trament concernant les functions anonymes et les closures.

    @Ivan Enderlin

    Effectivement, pour les stream wrappers, ça pourrait être intéressant. Par contre pour ce qui serait d'une "amélioration considérable", je demande à voir quand même.

  4. mageekguy, Thu Jun 03 2010

    Jusqu'à "L'état des traits" :).

    Sans rire, la documentation du Zend Engine est plus que restreinte, et si tu as un minimum de savoir à faire partager sur le sujet, je pense que pas mal de monde sera intéressé.

    Et je plussois pour les lambdas et les closures :).

  5. Olivier, Tue Jun 22 2010

    Félicitation pour ce brillant article. Féru de Javascript, cela faisait longtemps que j'espérais quelque chose en PHP qui me permettrait de composer des classes en réutilisant le code de classes "Outils".

    Après avoir lu ton article ainsi que le RFC, je suis bien déçu de découvrir que les traits sont "stateless", càd que l'on ne peut pas utiliser les variables membres de la classe. Voilà encore une possibilité offerte dans d'autre langage (avec brio par Javascript) qui se retrouve à moitié implémentée une fois sur PHP.

    Qu'est-ce qui empêche l'utilisation des variables membres ? Si l'on considère un objet comme une collection de clé/valeur et de comportements, qu'est-ce qui empêche les "traits" d'ajouter ou de modifier les clés d'un objet, après tout, ils modifient bien les comportements.

    Quand on voit comme il est facile avec MooTools de créer des classes en assemblant des morceaux de droites et de gauches, on trouve PHP bien triste et peu capable. Dommage.

  6. metagoto, Tue Jun 22 2010

    @Olivier

    Les traits sont stateless dans le sens où ils ne peuvent manipuler leur propres variables. Ils doivent recourir aux variables pré-définies de la classe client. A vrai dire, ces variables ne sont pas forcément pré-définies. Rien n'empêche un trait d'ajouter de nouvelles variables, mais celles-ci auront une visibilité publique, ce qui peut être gênant:

    function traitFunc() {
      // $newVar sera ajoutée à la classe client
      $this->newVar = 'value';
    }

    Cette contrainte spécifique aux traits par rapport aux variables (et donc leur état) est un choix délibéré. Il s'agit avant tout d'éviter les problèmes inhérents à l'héritage multiple (la duplication potentielle des états, comment les liens se font à l'exécution, comment les symboles sont "manglés" etc), le tout dans un cadre de composition "statique" car définie à la compilation.

    Par nature, javascript est plus dynamique que php car son modèle objet est basé sur les prototypes plutôt que les classes. De plus, tout est objet dans javascript. "this" est lié à l'objet courant (l'objet qui a le focus à un instant T dans l'exécution). La linéarité de la chaine des prototypes et le modèle clés/valeures fait des merveilles.

    php n'est tout de même pas si éloigné de javascript quand on considère la possibilité d'ajouter des membres à la volé. Uniquement des variables car les méthodes/functions ne sont pas des first-class objects. Cependant, avec l'arrivé des fonctions anonymes, et moyennant quelques améliorations syntaxiques, php est tout proche de "simuler" un comportement à la javascript. En tout cas, le trunk actuel intègre une implémentation du "Modified proposal A" dans cette RFC:

    http://wiki.php.net/rfc/closures/object-extension

    Des trucs sympas sont donc au programme ;)

  7. Olivier, Fri Jul 16 2010

    @metagoto: "Closures: Object extension" ? Alors ça c'est drôlement intéressant ! On pouvait toujours tricher avec __call, mais c'était ennuyeux.

    Pour en revenir aux traits, s'ils sont capable de modifier l'objet de la classe qui les utilise ça me va complètement, et je ne dirais pas de mal de PHP pendant une semaine au moins ;-)

    Merci pour ton explication.

Add a comment

Formating help