Metaprogramming in Ruby

Apr 7, 2014

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:

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 obj and my_method() is an intance method of MyClass.

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

  1. What object does the instance variable @x belongs to?
  2. What object should you call other_method on?

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.