un peu de métaprogrammation avec Ruby IntroductionCet article est disponnible ici : http://funkywork.blogspot.com/2011/12/metaprogrammation-et-ruby.html
et en PDF ici : http://nuki.music-all.be/tutos/meta.pdf
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éorieLe 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 MonkeyPatchingle 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).
- Code:
-
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 :
- Code:
-
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’sNous 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.
- Code:
-
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 tranchantBien 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 ConclusionLe 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.
- Code:
-
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 classesUn 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 :
- Code:
-
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 :
- Code:
-
$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 Je me confonds en excuses... (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.
- Code:
-
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
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 objetsEn 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 moduleBien 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:
- Code:
-
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
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:
- Code:
-
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
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:
- Code:
-
module AModule
def somme x, y
return x + y
end
end
class Some
extend AModule
end
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.
PratiqueNous 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 dynamiquementUne classe est une instance d’un objet Class, il est donc possible d’accèder à son constructeur.
- Code:
-
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
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 Je me confonds en excuses...Grâce aux concepts étudiés précédemment, nous allons pouvoir nous lancer progressivement dans la création d'un petit Je me confonds en excuses... 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.
- Code:
-
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
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 Je me confonds en excuses... comme par exemple dans la portion de code suivante:
- Code:
-
class Weapons < Tables
set_table_name :weapons
set_table_fields :id, :cost, :stats
set_default_value :default_data
end
ConclusionJ'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.