jeudi 15 décembre 2011

Métaprogrammation et Ruby

Cet article est disponible en PDF : Lien ver le fichier PDF

Introduction

1 Avant-propos
La métaprogrammation est une manière de manipuler des données et structures décrivant eux même des applications/programmes.
Il s’agit d’une manière de programmer qui peut réduire considérablement la taille d’une portion de code ainsi que la complexité de l’utilisation d’une bibliothèque, d’un outil. La métaprogrammation fait partie d’un ensemble de technique requierant de la rigueure mais pouvant apporter énormément à une application.
J’ai choisi d’étudier ce concept autour du langage de programmation Ruby, qui est un langage que je connais relativement bien et qui admet nativement des
concepts de métaprogrammation.
Je ne suis pas ingénieur et je ne serai pas en mesure d’aborder tous les concepts de métaprogrammation, cependant, considérez que cet article peut être une introduction accessible pouvant amener à la création d’un Domain specific language.
L’objectif de cet article est donc de spécifier certaines choses du langages pour permettre au lecteur d’étendre Ruby pour plus de flexibilité dans la rédaction de code.


2 La métaprogrammation ?
Dans le cas de la programmation orienté objet, la métaprogrammation est une forme de traitement qui vise à modifier la déscription d’un objet et/ou ses comportemments. Cela repose sur plusieurs concepts tels que la réflexivité, l’introspection et d’autre. Cependant, dans cette rédaction, nous nous focaliserons sur des techniques de métaprogrammation sans pour autant rentrer dans une approche excessivement formelle et abstraite. Retenons donc la métaprogrammation va nous aider à faciliter la résolution de problèmes parfois ennuyeux de 1manière élégante. Dans cet article nous n’aborderons pas des méthodes spécifique à la métaprogrammation, mais nous évaluerons des spécifités du langage utilise (et parfois logique) pour la métaprogrammation.




Théorie
Le Ruby est un langage adapté à la métaprogrammation pour des raisons simples : son expressivité, son modèle objet respectant relativement bien les normes du paradigme orienté classes (bien qu’il soit possible de fonctionner par prototypage). D’ailleurs, en programmant en Ruby, vous avez été naturellement confrontés à de la métaprogrammation. Par exemple, la génération automatique d’accesseur et de mutateurs qui utilise de la métaprogrammation (dont je parlerai plus tard).


3 Le MonkeyPatching

le monkeypatching est une manière de modifier/étendre du code sans en modifier la source. C’est relativement courant dans la programmation dite dynamique et cela peut servir à modifier correctement une application/bibliothèque sans en modifier la source. Ce qui rend donc une restauration de données d’origine très facile. Le Monkeypatching de ruby prend son sens avec les possibilité d’aliaser les méthodes. Pour rappel, un alias est une manière de renommer quelque chose. Donc grace aux alias, il sera possible de modifier le comportemment d’une méthode sans modifier la classe dans laquelle elle se trouve physiquement. Voici un exemple avec la classe Array pour laquelle je vais modifier la méthode push (Ajoute un objet dans un tableau) de manière à ce qu’elle nous indique l’objet ajouté. (Je suis conscient que ce n’est pas très pratique mais il ne s’agit que d’un exemple).
  1. class Array
  2.   # Alias de la méthode push
  3.   alias ancien_push push
  4.   # Redéfinition de push
  5.   def push object
  6.     # Appel de l'ancienne méthode
  7.     self.ancien_push object
  8.     print "Ajout de #{object.to_s}"
  9.   end
  10. end

Comme vous pouvez le voir, il est très facile de modifier une méthode. Cependant, il est aussi possible de greffer de nouvelles méthodes à une classe déjà existante. Par exemple, ajoutons une méthode getfirst, cette méthode ne sert a rien mais elle nous retournera le premier objet contenu dans notre tableau :


  1. class Array
  2.   def getfirst
  3.     return self[0]
  4.   end
  5. end


Ce qui nous amène a la conclusion que si une classe porte le même nom qu’une autre classe, elle se fusionneront. Cependant, sans alias, les méthodes seront écrasées. Donc a moins de ne modifier completement le traitement d’une méthode, évitons de réécrire du code inutilement et utilisons les alias.


3.1 Encore plus loin dans la création de patch’s
Nous avons vu qu’il était possible de greffer/modifier des méthodes aux classes. Il est aussi possible de greffer des méhodes à des objets. Par exemple, admettons que j’ai une variable qui contienne une chaine de caractère et que je veuille pouvoir utiliser sur cette variable une méthode qui me retournerait un booléen pour savoir si la première lettre de ma chaine est bien C. Ce genre de méthode ne serait utile que pour une seule variable et il serait dommage de modifier intégralement la classe String pour si peu.


  1. test_string = "Ce que je suis, un Chat"
  2. def test_string.verification
  3.   return (self =~ /^C/) != nil
  4. end


J’en conviens que cette méthode n’est pas vraiment utile, mais comme vous pouvez le voir, il est facile de greffer certaines composantes uniquement à certaines instances et non à toute une classe.




3.2 Une arme à double tranchant
Bien que vu sous cet aspect, le monkeypatching semble une solution agréable de modification de code et donc, par extension, de confort d’utilisation, je trouve (et ça n’engage que moi) que le monkeypatching pose aussi un réel problème de raisonnement du code. Bien qu’il soit très agréable de pouvoir jongler avec les méthodes du langage, la modification abusive entraîne rend souvent un code plus complexe a relire que si des classes spécifiques avaient été définies. Cependant, ce n’est qu’une opinion.



3.3 Conclusion
Le monkeypatching nous amène naturellement à l’affirmation que les classes sont ouvertes. Il est donc possible de les modifier à la volée. Ce qui amène donc à recommander une forme d’éducation de la part des développeurs pour éviter que cet excès de liberté n’amène à la déterioration d’un noyau de code. Je vous laisse analyser cette exemple pour comprendre cette mise en garde.
  1. class Fixnum
  2.   def +(obj)
  3.     self * obj
  4.   end
  5. end


Bien que cet exemple démontre une certaine stupidité de la part du développeur, il serait possible d’imaginer des exemples plus perfide et moins facilement détectable.




4 Un peu plus sur les classes
Un concept utile à la métaprogrammation est qu’en Ruby, les noms de classes sont des constantes. Il est donc possible d’instancier nos objets de cette manière :
  1. class Some
  2.   def initialize
  3.     @attribut = 10
  4.   end
  5. end
  6. classe = Some
  7. test = classe.new


Mais ce n’est pas tout à propos des classes. En Ruby, une classe est définie quand le programme est lancé et non à la compilation (car le langage est interprêté). Entre les blocks méthodes, il est aussi possible d’exécuter du code. Par exemple, imaginons une méthode changeant de comportemment en fonction de son contexte :
  1. $in_debug = true
  2. class Some
  3.   if $in_debug
  4.     def test
  5.       return "méthode en debug"
  6.     end
  7.   else
  8.     def test
  9.       return "méthode pas en debug"
  10.     end
  11.   end
  12. end
  13. t = Some.new
  14. print t.test


Cependant, sachez que cette exécution est effectuée à l'exécution du code et donc, même en changeant la valeur de la variable globale, la classe a été définie comme étant en debugmode.
Il est donc possible de définir les accesseur et les mutateurs au moyen d'une boucle. Je montre cet exemple uniquement pour donner des pistes vers la création d'un DSL (Domain specific language), cependant, je vous déconseille fortemment d'utiliser une boucle pour définir les accesseurs/mutateurs. Il ne s'agit, ici, que d'une simple expérience.
  1. class Some
  2.   for i in 0..3
  3.     attr_accessor "arg#{i}".to_sym
  4.   end
  5.   def initialize
  6.     @arg0 = 2
  7.     @arg1 =9
  8.     @arg2 =18
  9.     @arg3 =27
  10.   end
  11. end
  12. test = Some.new
  13. print test.arg2
la notion importante de cet exemple est que les classes sont définies à l'exécution et qu'il est donc possible de prendre beaucoup de raccourcis syntaxique.


4.1 Les classes sont des objets
En ruby (et dans d'autres langages de programmation orientés objets), une classe est avant tout une instance d'un objet Class. Il est donc tout a fait possible d'accéder au constructeur de Class (et de définir une classe en lui passant un block en paramètre). Cependant, cela va beaucoup plus loin.
Rappellons nous qu'il est possible d'exécuter des actions entre des méthodes en Ruby. C'est d'ailleurs sur ce principe que reposent les attr_accessor, attr_reader,attr_writter qui sont des méthodes qui définissent un comportemment sur l'instance de Classe et non sur la future instance de notre classe.


4.2 Utiliser les module
Bien que le comportemment primaire des modules soit de faire office d'espace nom, il est possible de faire ce qu'on appelle des Mixins. Il s'agit d'utiliser l'inclusion d'un module pour partager ses méthodes avec une classe. Par exemple:
  1. module AModule
  2.   def sayHello
  3.     print "Hello guy's, i'm #{@prenom} #{@nom}"
  4.   end
  5. end
  6. class Michael
  7.   include AModule
  8.   def initialize
  9.     @prenom = "Michael"
  10.     @nom = "Spawn"
  11.   end
  12. end
  13. mick = Michael.new
  14. mick.sayHello
Dans cet exemple, le module utilise les attributs de la classe et l'appelle de la méthode sayHello fonctionne parfaitement.
Si vous avez déjà souvent utilisé Ruby, vous savez qu'il est possible de définir des méthodes de classes de cette manière:
  1. class Some
  2.   class << self
  3.     def test x, y
  4.       return x + y
  5.     end
  6.   end
  7.   # Ou bien
  8.   def self.test2 x , y
  9.     return x * y
  10.   end
  11. end
En utilisant la théorie des mixins, il est possible de relier des méthodes à un contexte statique, donc en tant que méthode de classe (et non d'instance). Voici un exemple:
  1. module AModule
  2.   def somme x, y
  3.     return x + y
  4.   end
  5. end
  6. class Some
  7.   extend AModule
  8. end
  9. print Some.somme 9, 10
Comme on peut le voir dans l'exemple, il est donc possible d'étendre des méthodes au singleton d'une classe. Il faut évidemment prendre ces exemples dans des contextes pertinents, comme par exemple la réalisation d'une bibliothèque réutilisable. Tout ces petits exemples nous amènent petit à petit à la dernière partie de ce cours.





Pratique

Nous allons maintenant mettre en pratique les cas que nous avons soulevés précédemment sous forme de petits exemples rapides.


5 Construire une classe dynamiquement
Une classe est une instance d’un objet Class, il est donc possible d’accèder à son constructeur.


  1. uneClasse = Class.new do
  2.   attr_accessor :argument1
  3.   attr_accessor :argument2
  4.   def initialize args1, args2
  5.     @argument1, @argument2 = args1, args2
  6.   end
  7. end
  8. test = uneClasse.new 1, 2
  9. print test.argument2
Cet exemple assez naïf n'est ici que pour montrer qu'il est possible de créer des classes dynamiques au même titre que n'importe quelle objet.


6 Vers un premier DSL
Grâce aux concepts étudiés précédemment, nous allons pouvoir nous lancer progressivement dans la création d'un petit DSL pour faciliter l'utilisation de nos propres outils.
L'exemple fourni est très simple (et très inutile), il montre comment utiliser de manière intuitive, une méthode pour multiplier un argument par une valeur fournie.
  1. module AR
  2.   def bind_test *args
  3.     return @test if args.length == 0
  4.     @test = args[0]
  5.   end
  6. end
  7. class Some
  8.   extend AR
  9.   bind_test 99
  10.   attr_accessor :arg1
  11.   def initialize arg1
  12.     @arg1 = arg1 * (self.class).bind_test
  13.   end
  14. end
  15. test = Some.new(9)
  16. print test.arg1
Dans le cas d'une construction de librairie, le module AR n'est pas visible (enfin, pas situé dans la même portion de code), il est donc possible de simuler un véritable petit DSL comme par exemple dans la portion de code suivante:
  1. class Weapons < Tables
  2.   set_table_name :weapons
  3.   set_table_fields :id, :cost, :stats
  4.   set_default_value :default_data
  5. end


Conclusion
J'achève ici cette sommaire présentation de certains concepts liés à la métaprogrammation. Il s'agit de méthodes qui peuvent être très amusantes à rédiger. Cependant, il ne s'agit pas d'une approche très orientée "concepts précis" mais plutôt d'une explication générale sur certains concepts relatifs à Ruby pour la construction orienté métaprogrammation. Ce genre de techniques sont généralement utilisées pour le développement de librairies. Je doute qu'il soit réellement nécéssaire de déployer ce genre de méthode dans des petites applications.
Je vous remercie d'avoir lu mon article.
Michaël.

Aucun commentaire:

Enregistrer un commentaire