Zend certified PHP/Magento developer

RubySource: Ruby Metaprogramming: Part I

If you’re working with Ruby, chances are by now you’ve heard the word “metaprogramming” thrown around quite a lot. You may have even used metaprogramming, but not fully understood the true power or usefulness of what it can do. By the end of this article, you should have a firm grasp not only of what it is, but also what it capable of, and how you can harness one of Ruby’s “killer features” in your projects.

What is “metaprogramming”?

Metaprogramming is best explained as programming of programming. Don’t let this abstract definition scare you away though, because Ruby makes metaprogramming as easy to understand as it is to work with.

Metaprogramming can be used a way to add, edit or modify the code of your program while it’s running. Using it, you can make new or delete existing methods on objects, reopen or modify existing classes, catch methods that don’t exist, and avoid repetitious coding to keep your program DRY.

Understanding how Ruby calls methods

Before you can understand the full scope of metaprogramming, it’s imperative that you grasp how Ruby finds a method when it is called. When you call a method in Ruby, it must locate that method (if it exists) within all the code that is within the inheritance chain.

class Person
  def say
    "hello"
  end
end
john_smith = Person.new
john_smith.say # = "hello"

When the say() method is called in the example above, Ruby first looks for the parent of the john_smith object; because this object is an instance of the Person class, and it has available a method called say(), this method is called.

Things get more complicated however when the object is an instance of a class which has inherited from another class:

class Animal
  def eats
    "food"
  end
  def lives_in
    "the wild"
  end
end
class Pig  Animal
  def lives_in
    "farm"
  end
end
babe = Pig.new
babe.lives_in # = "farm"
babe.eats # = "food"
babe.thisdoesnotexist # = NoMethodError: undefined method `thisdoesnotexist' for Pig:0x16a53c8

When we introduce inheritance into the mix, Ruby needs to consider methods defined higher in the inheritance chain. When we call babe.lives_in(), Ruby first checks the Pig class for the method lives_in(); because it exists, it’s called.

It’s a slightly different story when we call the babe.eats() method, though. Ruby checks for that method by asking the Pig class if it can respond to eats(), and in the absence of that method existing on Pig, it will continue by asking the parent class Animal if it can respond; it can in our case, so it will be called.

When we call babe.thisdoesnotexist(), because the method does not exist, we get a NoMethodError exception. You can think of this as a sort of cascade: whatever method is defined lowest in the inheritance chain will be the method that ends up being called; if the method doesn’t exist at all, an exception is raised.

Based on what we have discovered so far, we can summarise the way Ruby looks up each method as a pattern something like the following:

  • Ask the object’s parent class if it can respond to the method, calling it if found.
  • Ask the next parent class up if it can respond to the method and call it if found, continuing this step towards the top of the inheritance chain for as long as necessary.
  • If nothing in the inheritance chain can respond to the method being called, the method does not exist and an Exception should be raised.

Note that because every object inherits from Object (or BasicObject in Ruby 1.9) at the very top level, that class will always be the last to be asked, but only if it makes it that far up the inheritance chain without finding a method that can respond.

Introducing the Singleton class

Ruby gives you the full power of Object Oriented programming and allows you to create objects that inherit from other classes and call their methods; but what if only a single object requires an addition, alteration, or deletion?

The “singleton class” (sometimes known as the “Eigenclass”), is designed exactly for this and allows you to do all that and more. A simple example is in order:

greeting = "Hello World"
def greeting.greet
  self
end
greeting.greet # = "Hello World"

Let’s digest what’s just happened here line by line. On the first line, we create a new variable called greeting that represents a simple String value. On the second line, we create a new method called greeting.greet, and give it a very simple content. Ruby allows you to choose what object to attach a method definition to by using the format some_object.method_name, which you may recognise as the same syntax for adding class methods to classes (ie. def self.something). In this case, as we had greeting first, the method has been attached to our greeting variable. On the final line, we call the new greeting.greet method we just defined.

The greeting.greet method has access to the entire object it has been attached to; in Ruby, we always refer to that object as self. In this case, self refers to the String value we attached it to. If we had attached it to a Array, self would have returned that Array object.

As you’re about to see, adding methods using some_object.method_name is not always the best way to do such tasks. That’s why Ruby provides another far more useful way of working with objects and their methods in a dynamic way. Meet the Singleton class:

greeting = "i like cheese"
class  greeting
  def greet
    "hello! " + self
  end
end
greeting.greet # = "hello! i like cheese"

The syntax is very strange, but the outcome is the same as our some_object.method_name way of adding methods. This singleton class method allows you to add many methods at once without having to prefix all of your method names. This syntax also allows you to add anything you would normally add during the declaration of a class, including attr_writer, attr_reader, and attr_accessor methods.

How does it work?

So how does it actually work? The name “singleton class” might have given this away slightly, but Ruby is sneaky and adds another class into our inheritance chain. When you try to operate on the singleton class, Ruby needs a way to add methods to the object we’re adding to, something that the language does not allow. To get around this it creates a secret new class, which we call the “singleton class”. This class is given the methods and changes instead, is made the parent of the object we’re working on. This singleton class is also made an instance of the previous parent of our object so that the inheritance chain remains mostly unchanged:

some object instance  singleton class  parent class  ...  Object/BasicObject

Returning to what we know about Ruby method lookup, we previously decided that there was three simple steps that the Ruby interpreter followed when looking up a method. Singleton classes add one extra step to this lookup process:

  • Ask the object if its singleton class can respond to the method, calling it if found.
  • Ask the object’s parent class if it can respond to the method, calling it if found.
  • Ask the next parent class up if it can respond to the method and call it if found, continuing this step towards the top of the inheritance chain for as long as necessary.
  • If nothing in the inheritance chain can respond to the method being called, the method does not exist and an Exception should be raised.

As we previously discussed, you can think of this as a cascade, and that has a very important impact on our idea of objects in Ruby: not only can Objects gain methods from their inherited classes, but now they can gain individually unique methods as the program is running.

Putting metaprogramming to work with instance_eval and class_eval

Having singleton classes is all good and well, but to truly work with objects dynamically you need to be able to re-open them at runtime within other functions. Unfortunately, Ruby does not syntactically allow you to have any class statements within a function. This is where instance_eval comes into the picture.

The instance_eval method is defined in Ruby’s standard Kernel module and allows you to add class methods to an object just like our singleton class syntax.

foo = "bar"
foo.instance_eval do
  def hi
    "you smell"
  end
end
foo.hi # = "you smell"

The instance_eval method can take a block (which has self set to that of the object you’re operating on), or a string of code to be evaluated. Inside the block, you can define new methods as if you were writing a class, and these will be added to the singleton class of the object.

Methods defined by instance_eval will be class methods, even if logically it seems to be named wrongly; it’s given it’s name because of the singleton class it creates on the instance against which it’s evaluated. It’s important to note that the scope is that of class methods, because it means you can’t do things like attr_accessor as a result. If you find yourself needing this, you’ll want to use something like class_eval instead, which adds (non-surprisingly by our reverse logic) instance methods. The class_eval method is used very similarly to instance_eval, but must be applied to a Class object and not to any old object. An example:

bar = "foo"
bar.class.class_eval do
  def hello
    "i can smell you from here"
  end
end
bar.hello # = "i can smell you from here"

As you can see, instance_eval and class_eval are very similar, but their scope and application differs ever so slightly. You can remember what to use in each situation by remembering that they are opposite to what their name suggests: use instance_eval to make class methods, and use class_eval to make instance methods.

Why do I care?

As this point you’re probably thinking “that’s great, but why should I care? What real world value does any of this have?” The simple answer is “yes you should” and “a lot”.

Metaprograming allows you to create more flexible code, be it through beautiful APIs or easily testable code. More than that though, it allows you to do that using powerful and elegant programming techniques and syntax. Metaprogramming allows you to create code that is DRY, highly reusable, and extremely concise.

In part two of this series we’ll look at how to apply Metaprogramming to everyday problems, and see how the elegance and power of it can change the way you develop solutions forever.