Introduction
1 Avant-proposLa 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
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).
class Array # Alias de la méthode push alias ancien_push push # Redéfinition de push def push object # Appel de l'ancienne méthode self.ancien_push object print "Ajout de #{object.to_s}" end 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 :
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.
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.
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
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 :
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.
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:
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:
5 Construire une classe dynamiquement
Une classe est une instance d’un objet Class, il est donc possible d’accèder à son constructeur.
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.
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.
class Array def getfirst return self[0] end 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.
test_string = "Ce que je suis, un Chat" def test_string.verification return (self =~ /^C/) != nil 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.
class Fixnum def +(obj) self * obj end 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 :
class Some def initialize @attribut = 10 end end classe = Some 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 :
$in_debug = true class Some if $in_debug def test return "méthode en debug" end else def test return "méthode pas en debug" end end end t = Some.new 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.
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.
class Some for i in 0..3 attr_accessor "arg#{i}".to_sym end def initialize @arg0 = 2 @arg1 =9 @arg2 =18 @arg3 =27 end end test = Some.new print test.arg2
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:
Dans cet exemple, le module utilise les attributs de la classe et l'appelle de la méthode sayHello fonctionne parfaitement.
module AModule def sayHello print "Hello guy's, i'm #{@prenom} #{@nom}" end end class Michael include AModule def initialize @prenom = "Michael" @nom = "Spawn" end end mick = Michael.new mick.sayHello
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:
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:
class Some class << self def test x, y return x + y end end # Ou bien def self.test2 x , y return x * y end end
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.
module AModule def somme x, y return x + y end end class Some extend AModule end print Some.somme 9, 10
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.
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.
uneClasse = Class.new do attr_accessor :argument1 attr_accessor :argument2 def initialize args1, args2 @argument1, @argument2 = args1, args2 end end test = uneClasse.new 1, 2 print test.argument2
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.
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:
module AR def bind_test *args return @test if args.length == 0 @test = args[0] end end class Some extend AR bind_test 99 attr_accessor :arg1 def initialize arg1 @arg1 = arg1 * (self.class).bind_test end end test = Some.new(9) print test.arg1
class Weapons < Tables set_table_name :weapons set_table_fields :id, :cost, :stats set_default_value :default_data 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.