Pour suivre cet article sur les metaclass ruby, il faut bien avoir en tête certaines bases du langage ruby.
Soupçonner l’existence des metaclass
Ruby vous permet de définir et redéfinir à volonté des méthodes sur n’importe quel objet :
o = Object.new
def o.hello
"Hello !"
end
puts o.hello #=> "Hello !"
puts Object.new.hello #=> undefined method `hello'
Ici on a définit la méthode hello uniquement pour une instance de Object : o. Les autres instances d’Object n’auront pas cette méthode.
Comment est ce possible, puisque seuls les objets de type Class peuvent contenir des méthodes ?
Il s’avère que la mécanique interne de Ruby créé ce qu’on appelle une metaclass pour contenir ces méthodes. Mais venons en au vif du sujet :
Qu’est ce qu’une metaclass ?
Si je devait définir moi même ce qu’est une metaclass, je dirai que c’est une classe cachée rattachée à un objet en particulier :
c’est une classe : elle peut donc contenir des méthodes.
cachée : les metaclass font plus ou moins partie de la magie de ruby et il n’est pas simple d’accéder ou de manipuler celles ci. Nous allons y revenir.
elle est spécifique à un objet, chaque objet a son unique metaclass.
Il y a une syntaxe particulière pour accéder au contexte d’une metaclass. Ca ne s’invente pas :
class Foo
class << self
# Ici on se trouve dans le contexte de la metaclass Foo
puts self
end
end #=> #<Class:Foo>
puts Foo #=> Foo
puts Foo.new #=> #<Foo:0xb7c6b148>
Le résultat special #<Class:Foo> est spécifique aux metaclasses. Il faut le comprendre comme : "instance de Class ratachée à Foo".
A partir de l’exemple précédent, on peut définir une méthode générique sur la classe Object pour accéder à la metaclass de n’importe quel objet :
class Object
def meta
class << self
self
end
end
end
Ainsi on peut écrire directement :
class Foo
end
puts Foo.meta #=> #<Class:Foo>
puts Foo.new.meta #=> #<Class:#<Foo:0xb7cb19cc>>
On remarque bien que la metaclass d’une instance de Foo est spécifique à l’instance #<Foo:0xb7cb19cc> en particulier.
Définir des méthodes sur les metaclass
Vérifions ce que nous avons dit au départ, à savoir que les metaclass définissent des méthodes pour l’objet auquel elle est rattachée :
class Foo
end
f = Foo.new
def f.hello
puts "Hello !"
end
f.hello #=> Hello !
puts f.meta.instance_methods.include?("hello") #=> true
puts f.singleton_methods.include?("hello") #=> true
La méthode définie directement sur l’objet f est bien contenue dans la metaclass de l’objet f.
class Foo
end
f = Foo.new
class << f
def hello
puts "Hello !"
end
end
f.hello #=> Hello !
puts f.meta.instance_methods.include?("hello") #=> true
puts f.singleton_methods.include?("hello") #=> true
De la même manière, une méthode d’instance définie directement sur la metaclass de f la rend disponible pour cet objet.
Comme vous le savez maintenant parfaitement, les classes sont elles même des objets, il est donc tout à fait possible, et même courant ; d’utiliser les metaclass pour leur définir des méthodes. En fait, sans le savoir c’est ce que vous faisiez depuis toujours :
class Foo
def self.hello
puts "Hello !"
end
end
Foo.hello #=> Hello !
puts Foo.meta.instance_methods.include?("hello") #=> true
puts Foo.singleton_methods.include?("hello") #=> true
Et donc, on peut écrire également :
class Foo
end
class << Foo
def hello
puts "Hello !"
end
end
Foo.hello #=> Hello !
puts Foo.meta.instance_methods.include?("hello") #=> true
puts Foo.singleton_methods.include?("hello") #=> true
Tout cela est cohérent, continuons.
Gestion unifiée
La gestion des classes est similaire à la gestion de n’importe quel autre objet. Ceci est un aspect qui m’a séduit. Les classes définissent des méthodes pour leurs instances, et si on veut que ces classes aient des méthodes elles aussi, alors on fait la même chose en faisant appel à leur metaclass. Cette vision unifiée est extrêmement agréable et rend sa compréhension et son utilisation facile et intuitive après un peu d’entraînement ( si si ! ).
La classe Objet définit la méthode singleton_methods. Elle renvoie la liste des méthodes applicables sur l’objet receveur, que le receveur soit une classe ou une instance de Foo (par exemple).
Matz’s Ruby Interpreter (MRI)
L’implémentation en C de l’interpréteur ruby de Matz définit 3 structure de base :
struct RBasic {
unsigned long flags;
VALUE klass;
};
struct RObject {
struct RBasic basic;
struct st_table *iv_tbl;
};
struct RClass {
struct RBasic basic;
struct st_table *iv_tbl;
struct st_table *m_tbl;
VALUE super;
};
Un object au sens ruby est un RObject en C. Il définit :
une référence à une RClass : value klass
des flags d’état : unsigned long flags ( ils servent à stocker des informations comme taint, freeze, etc. )
une map de variable d’instance : struct st_table *iv_tbl
Une classe au sens ruby est une RClass C avec :
une référence vers un RClass, des flags et des variables d’instance comme pour un Object. C’est la raison pour laquelle on considère qu’une classe ruby rempli toutes les conditions pour être considéré comme un objet.
une map de méthodes indexée par leur nom : struct st_table *m_tbl
une référence vers une RClass super qui représente généralement la superclass ruby : VALUE super
Un point important sur les relations de ces structures C :
Pour toute RClass rc, la super RClass de la klass de rc est la klass de la super RClass de rc.
C’est compliqué, oui, je sais. J’ai mis du temps à le comprendre. C’est d’autant plus dur à comprendre que toute cette logique est cachée en ruby, et qu’il est difficile de le prouver avec un simple irb. Il est bien plus simple de se faire une idée avec le graph suivant :

Cela correspond à :
class Foo
end
class Bar < Foo
end
Ainsi on voit que :
la klass de la super RClass de Bar est : la metaclass de Foo
la super RClass de la klass de Bar est : la metaclass de Foo
C’est juste la manière dont c’est codé. Tout ceci est indétectable du point de vue de ruby :
class Foo
end
class Bar < Foo
end
puts Bar.meta.superclass #=> #<Class:Class>
puts Bar.superclass.meta #=> #<Class:Foo>
Ruby considère que la superclass de toute metaclass est la metaclas de Class.
Ordre de recherche d’une méthode
Lorsque l’on appelle une méthode sur un objet O, comment ruby fait il pour retrouver cette méthode ?
d’abord, il cherche dans les méthodes de la RClass du RObject correspondant à O
ensuite il cherche récursivement dans les super RClass
Dans le graph précédent, un appel à Bar.new.hello suivra le chemin de recherche suivant :
recherche de la méthode dans Bar
recherche de la méthode dans Foo
recherche de la méthode dans Object
Ceci assure l’héritage des "méthodes d’instance" (héritage classique).
Similairement, un appel de méthode comme Bar.hello suivra le schéma :
recherche de la méthode dans Bar.meta
recherche de la méthode dans Foo.meta
recherche de la méthode dans Object.meta
recherche de la méthode dans Class
Ceci assure l’héritage pour les "méthodes de classe".
Classes ouvertes
La définition des classes n’est jamais finie, elles restent ouvertes :
class Foo
def self.load_hello
def hello
puts "Hello !"
end
end
end
begin
Foo.new.hello #=> dynamic_methods.rb:9: undefined method `hello' for ...
rescue
Foo.load_hello
Foo.new.hello #=> Hello !
end
La méthode hello a été chargée dynamiquement.
Etendre des classes
Pour étendre un model existant, comme ActiveRecord par exemple, il y a une manière de faire qui n’est pas obligatoire mais reconnue et largement adoptée par la communauté.
Dans la série des acts_as_*, on va faire le notre, très simple : acts_as_taggable, dont l’objectif va être de rendre les instances d’une classe "taggable", on pourra leur attacher un tag.
La convention générale est d’intégrer une fonctionnalité de manière à avoir l’inclusion d’un module sur une classe mère ( comme ActiveRecord ::Base ), puis d’appeler la feature directement sur les classe voulues ( model.rb ) :
require 'acts_as_taggable'
class MyBase
include Acts::Taggable
end
class Foo < MyBase
acts_as_taggable :info
end
class Bar < MyBase
acts_as_taggable :comment
end
Notre classe mère sera MyBase.
D’abord on va créer dans le fichier ’acts_as_taggble.rb’ le module Acts ::Taggable :
module Acts
module Taggable
def self.included(base)
puts self #=> Affiche Acts::Taggable
# adds ClassMethods instance methods to the class object 'base'
base.extend(ClassMethods)
end
module ClassMethods
def acts_as_taggable( name )
puts self #=> Affiche Foo, puis Bar
end
end
end
Lorsque le module va être inclu par MyBase, la méthode included sera appelée sur le module Acts ::Taggable, en passant en paramètre la classe qui l’inclue.
A cette objet Class, qui est MyBase dans notre exemple, on va rajouter les méthodes d’un module généralement nommé ClassMethods. Ici, acts_as_taggable sera ajoutée aux singleton_methods de Foo ( ca sera bien une méthode de classe )
Cette méthode acts_as_taggable va être utilisée dans la déclaration des classes (elle va s’appliquer sur self=la classe). Ainsi dans la méhode acts_as_taggable on aura la variable self fixée à la classe tagguée.
On va alors définir dynamiquement des méthodes sur ces classes :
module Acts
module Taggable
def self.included(base)
# adds ClassMethods instance methods to the class object 'base'
base.extend(ClassMethods)
end
module ClassMethods
def acts_as_taggable( name )
class_eval <<-END_CE
def #{name}=(t)
@tag=t
end
def #{name}
@tag
end
END_CE
end
end
end
end
La méthode class_eval exécute du code dans le contexte du module courant. Ici, cela va rajouter les méthodes d’instance info et info= à la classe Foo, et comment et comment= à la classe Bar. Ces méthodes sont les accesseurs de la variable d’instance @tag qui stocke la valeur du tag.
require 'model'
f = Foo.new
f.info = "This class is beautiful !"
b = Bar.new
b.comment = "I'm just another class"
puts f.info #=> "This class is beautiful !"
puts b.comment #=> "I'm just another class"
Voila
Voila c’est finit pour cet article. J’espère que certains y trouveront des réponses et/ou des questions :) A bientôt !
Resources
Une conférence sur infoQ de Dave Thomas sur des bases de la metaprogrammation ruby
Un article expliquant les metaclass et leur utilité
Un article de Patrick Farley détaillant les implémentations de la MRI
Le ruby hacker guide qui détaille au chapitre 4 l’implémentation de la MRI