Metaprogramming in Ruby
For a long time Ruby metaprogramming and its tricks were like magic for me, something that only gurus reach. After some time and some books I started to realize that metaprogramming has nothing related with magic and everything started to have more sense. Here I'll try to write down things that I've read when I started to learn about ruby metaprogramming. Most of this content is from the excelent book "metaprogramming Ruby" of Paolo Perrota.
Metaprogamming is writting code that writes code. Cool things that we could do with this kind of magic:
- Write a wrapper to an external API, the wrapper routes each method to the API. Later if API changes you don't have to change your wrapper it will support new methods instantly.
- Own quick extensible DSLs to write your own language.
- You can remove duplication from your Ruby program at a level that Java programmers can only dream of.
class keyword is like a scope operator than instead a class declaration. This detail is what allows us create classes if they don't yet exists but it's also what allows us always open existing classes.
Here is an example where we define a class
Song, then we create an instance of that class (
song1) later we call to an non-existent method over this last created object. We get an 'undefined method' error so we open again the class
Song, define the missing method and back to call the same method. This is what we can do thanks to the class semantic.
Open class technique should be used carefully because you could redefine an existing method. This technique is know as "Monkey see monkey patch".
class Song def initialize(title) @title = title end end => nil
>> song1 = Song.new('hello world..') => #<Song:0x007fcd7ca03f60 @title="hello world.."> >> song1.play => NoMethodError: undefined method `play' for #<Song:0x007fcd7ca03f60 @title="hello world..">
class Song def play puts 'lara lara lara!!!!' end end # => nil
>> song1.play lara lara lara!!!! # => nil
Everything in Ruby is an object. All objects have an identity (
object_id); they can hold state and manifest behaviour by responding to messages (methods calls). The object state is given by instance variables, they just spring into existence when you assign them a value so you can have objects of the same class that carry different set of instance variables and of course different values too.
"a".object_id # => 70260267834120 "a".object_id # => 70260267812220 class A end class B < A end A.new.instance_of?(A) # => true B.new.instance_of?(A) # => false
class MyClass def my_method @v = 1 end end obj = MyClass.new # => #<MyClass:0x007fcd7d052948> obj.class # => MyClass obj.instance_variables # =>  obj.my_method obj.instance_variables # => [:@v]
Objects besides instance variables also have methods, you can print all methods that object will respond
>> obj.methods # => [:my_method, :local_methods, :ri, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup, ...] >> obj.methods.grep(/my/) # => [:my_method]But if you check the object you can't see where methods are, the only thing that there is inside the object is a link to the object class
>> obj => #<MyClass:0x007fcd7d052948>
From this image we can see that object's methods are stored in object's class. In the image
@v is named instance variable of
my_method() is an intance method of
An object's instance variable live in the object itself, and object methods live in object's class. That's why objects of the same class share methods but don't share instance variables.
We says previously "in Ruby everything is an object", then classes are objects too and everything that we applied to object so far also can be used in a class.
>> "hello".class # => String >> String.class # => Class >> Class.instance_methods(false) # => [:allocate, :new, :superclass] >> Class.superclass # => Module >> Class.ancestors #=> [Class, Module, Object, Kernel, BasicObject]
So a class is a souped module with 3 additional methods
Constants is any reference that begins with an uppercase letter, including the names of classes and modules
Module M MyConst = "blah" class C MyConst = "buu" end end
M C MyConst MyConst
The path of constants
:: is the operator to find a constant in a path akin c++ operator
Module M class C X = "a const" end ::C::X # 'a const' end
M.constansts # => [:C, :X] Module.constants # => [:Object, :Module]
Method Lookup goes "one step to the right, then up"
class MyClass def my_meth ; 'method' ; end end class MySubClass < MyClass end obj = MySubClass.new obj.my_meth # => 'method'
Module Lookup the ancestors chain goes from class to superclass. Actually, the ancestors chain also includes modules
module M def my_meth ; 'M#my_meth' ; end end class C include M end class D < C end D.new.my_meth # => "M#my_meth" D.ancestors # => [D, C, M, Object, Kernel, BasicObject]
when you include a module in a class (or even in another module) Ruby plays a little trick. It creates an anonymous class that wrap the module and inserts the anonymous class in the chain, just above the including class itself (proxy classes).
Method execution the ancestors chain goes from class to superclass. Actually, the ancestors chain also includes modules.
def my_method temp = @x + 1 another_method(temp) end
Once the method lookup find a method like
my_method we have to execute it, to do that we need to figure out
- What object does the instance variable
- What object should you call
Discovering self every line of Ruby code is executed inside an object the so called current object (self). Only one object can take the role of self at a given time, but no object holds that role for a long time.
In particular, when you call a method, the receiver becomes self. From that moment on, all instance variables are instance variables of self, and all methods called without an explicit receiver are called on self
Sumarizing when you call a method Ruby looks up it following "one step to right then up" rule and then executes the method with the receiver as self. Understanding the way that ruby use to find and to execute methods is the first step to learn how to do metaprogrammation.