Don't Know Metaprogramming In Ruby?

Don’t Know Metaprogramming In Ruby?

This guest post is by Gavin Morrice, Managing Director of Katana Code Ltd., a software boutique based in Edinburgh, Scotland. He likes sharing Rails tips on his site. When he’s not writing code he’s usually weightlifting, reading or writing

Introduction

Gavin
Morrice

One of the most impressive aspects of Ruby is its metaprogramming capabilities. As a dynamic language, Ruby gives you the freedom to define methods and even classes during runtime. Metaprogramming with Ruby, one can do in a few minutes what other languages may take hours to do. By cleverly planning your code and applying the techniques mentioned here, you’ll be able to write code that is DRYer, lighter, more intuitive and more scalable.

This tutorial assumes you are already familiar with the following concepts:

  • Ruby Classes and class inheritance
  • Instance methods and instance variables
  • Raising exceptions

and standard Ruby notations:

  • to show that method_name is a method in MyClass
    • MyClass#method_name
  • to show the return value of a method
    • =>

What is Metaprogramming?

According to Wikipedia:

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime. In many cases, this allows programmers to get more done in the same amount of time as they would take to write all the code manually, or it gives programs greater flexibility to efficiently handle new situations without recompilation. Or, more simply put: Metaprogramming is writing code that writes code during runtime to make your life easier.

Adding methods in the context of an object

In Ruby, everything is an object. The base class in Ruby is called Object (or BasicObject in Ruby 1.9) and all other classes inherit properties from it. Every object in Ruby has its own methods, and instance variables which can be added, edited or removed during runtime. Here is a simple example:

# Example 1: create a new instance of class Object
my_object = Object.new

# define a method on my_object to set the instance variable @my_instance_variable
def my_object.set_my_variable=(var)
  @my_instance_variable = var
end

# define a method on my_object to return value of instance variable @my_instance_variable
def my_object.get_my_variable
  @my_instance_variable
end

my_object.set_my_variable = "Hello"
my_object.get_my_variable # => Hello

In this example, we have created a new instance of the Object class and defined two methods on that instance for writing and reading (setting and getting). The two new methods that we’ve defined are only available to our object my_object and will not be present on any other instance of the Object class. We can prove this by extending our example like so:

# Example 2: create a new instance of class Object
my_object = Object.new

# create a second instance of class Object
my_other_object = Object.new

# define a method on my_object to set the instance variable @my_instance_variable
def my_object.set_my_variable=(var)
  @my_instance_variable = var
end

# define a method on my_object to return value of instance variable @my_instance_variable
def my_object.get_my_variable
  @my_instance_variable
end

my_object.set_my_variable = "Hello"
my_object.get_my_variable # => Hello

my_other_object.get_my_variable = "Hello" # => NoMethodError

When we try to call get_my_variable() on our second object my_other_object the interpreter raises a NoMethodError to tell us that my_other_object doesn’t recognise the method get_my_variable().

To see where this feature of Ruby might be useful, let’s look at a more common example: writing class methods.

Writing Class Methods

You’ll probably already be aware of this common syntax for writing methods to your Ruby classes:

# Example 3
class MyClass
  def self.capitalize_name
    name.upcase
  end
end
print MyClass.capitalize_name # => MYCLASS

Within our class definition we’re defining a method on one particular object just like we did in Example 1. Only, this time the object is self (in this case MyClass). And as we saw in Example 2, the capitalize_name() method is only available to this particular object MyClass and no other class (yet). This is just one example of how to write a class method. To refer back to Example 3 again, here are three different approaches to defining the same class methods:

# Example 4
# approach 1
class MyClass
  # defining a class method within the class definition
  def MyClass.capitalize_name
    name.upcase
  end
end

# approach 2
class MyClass;end
# defining a class method out with the class definition
def MyClass.capitalize_name
  name.upcase
end

# approach 3
# define a new class named MyClass
MyClass = Class.new
# add the capitalize_name to MyClass
def MyClass.capitalize_name
  name.upcase
end

See how similar approach 3 here is to Example 1? You will hopefully have realised that when you write a class method in Ruby, it’s just the same as creating an instance of any class and adding methods only to that instance, only this time its an instance of the class “Class”.

Writing Code That Writes Code

An important philosophy in programming is DRY (Don’t Repeat Yourself). Writing code that is the same (or similar) several times is not only an inefficient waste of time, it can become a nightmare when you need to make changes in future. In many cases, it’s possible to remove this duplication of effort by writing code that writes the code for you. Here’s an example… Consider an application for a car manufacturer to store and access data on each of it’s models. Within the application we have a class called CarModel:

# Example 5
class CarModel
  def engine_info=(info)
    @engine_info = info
  end

  def engine_info
    @engine_info
  end

  def engine_price=(price)
    @engine_price = price
  end

  def engine_price
    @engine_price
  end

  def wheel_info=(info)
    @wheel_info = info
  end

  def wheel_info
    @wheel_info
  end

  def wheel_price=(price)
    @wheel_price = price
  end

  def wheel_price
    @wheel_price
  end

  def airbag_info=(info)
    @airbag_info = info
  end

  def airbag_info
    @airbag_info
  end

  def airbag_price=(price)
    @airbag_price = price
  end

  def airbag_price
    @airbag_price
  end

  def alarm_info=(info)
    @alarm_info = info
  end

  def alarm_info
    @alarm_info
  end

  def alarm_price=(price)
    @alarm_price = price
  end

  def alarm_price
    @alarm_price
  end

  def stereo_info=(info)
    @stereo_info = info
  end

  def stereo_info
    @stereo_info
  end

  def stereo_price=(price)
    @stereo_price = price
  end

  def stereo_price
    @stereo_price
  end
end

Each car model comes with various features such as “stereo”, “alarm” etc. We have a method to get and set the values of each feature of the car. Each feature has information and price so for every new feature we add to the CarModel class, we need to define two new methods: feature_info and feature_price. Since each of these methods are similar, we can do the following to simplify this code:

# Example 6
class CarModel
  FEATURES = ["engine", "wheel", "airbag", "alarm", "stereo"]

  FEATURES.each do |feature|
    define_method("#{feature}_info=") do |info|
      instance_variable_set("@#{feature}_info", info)
    end

    define_method("#{feature}_info") do
      instance_variable_get("@#{feature}_info")
    end

    define_method "feature_price=" do |price|
      instance_variable_set("@#{feature}_price", price)
    end

    define_method("#{feature}_price") do
      instance_variable_get("@#{feature}_price")
    end
  end
end

In this example, we start by defining an array called FEATURES which includes all the features we wish to add methods for. Then, for each feature, we use Ruby’s Module#define_method to define four methods for each feature. Just like in Example 5, the four methods are getter and setter methods for the feature’s price and info. The only difference is, they have been written dynamically when the class is defined and not by us. We use Object#instance_variable_set() to set the value of instance variables for each feature and Object#instance_variable_get to return the values for each.

The need to define getter and setter methods like this is quite common in Ruby so it’s not surprising that Ruby already has methods that do just that. Module#attr_accessor can be used to do the same thing as in Example 6 with just a single line of code.

# Example 7
class CarModel
  attr_accessor :engine_info, :engine_price, :wheel_info, :wheel_price, :airbag_info, :airbag_price, :alarm_info, :alarm_price, :stereo_info, :stereo_price
end

Great! But this still isn’t ideal. For each feature, we still need to define two attributes (feature_info and feature_price). Ideally we should be able to call a method that can do the same as in Example 7 but by only listing each feature once.

# Example 8
class CarModel
  # define a class macro for setting features
  def self.features(*args)
    args.each do |feature|
      attr_accessor "#{feature}_price", "#{feature}_info"
    end
  end

  # set _info and _price methods for each of these features
  features :engine, :wheel, :airbag, :alarm, :stereo
end

In this example, we take each of the arguments for CarModel#features and pass them to attr_accessor with _price and _info extensions. Although this approach is slightly more involved than the one in Example 7, it ensures that each feature is treated the same and means that adding more attributes in future will be simpler.

A Brief Explanation of the Ruby Object Model

Before going any further, it’s important that we understand the basics of Ruby’s Object Model and how Ruby deals with method calls. Whenever you call a method on an object, the interpreter first looks through the object’s instance methods to see if it can find that method. If the interpreter can find the method, it will execute it as expected but if not, it will pass the request up the chain to the object’s class. If it can’t find the method there it will continue to look in that class’s parent class, then the parent’s parent etc. up to the Object class itself. But it doesn’t stop there… If the interpreter can’t find the method anywhere up the object’s chain of inheritance, it will go back to the object and call another method called method_missing(). Just like with our first method, the interpreter looks for method_missing() in the object’s methods, then the object’s class’s instance methods etc. until reaches the Object class where method_missing() is defined and will raise a NoMethodError error. This is when metaprogramming really starts getting fun!

By defining method_missing() yourself within a class, it’s possible to change this default behaviour for some pretty useful effects. method_missing() is passed two arguments; the name of the missing method (as a symbol) and array of its arguments. Let’s look at an example:

# Example 9
class MyGhostClass
  def method_missing(name, *args)
    puts "#{name} was called with arguments: #{args.join(',')}"
  end
end

m = MyGhostClass.new
m.awesome_method("one", "two") # => awesome_method was called with arguments: one,two
m.another_method("three", "four") # => another_method was called with arguments: three,four

There’s no method named awesome_method() or another_method() within our class yet when we try calling it, we don’t see the usual NoMethodError. Instead, we see the name of the methods and their arguments, just like we defined in method_missing().

We can expand this idea a little more by adding conditions to this method. Let’s say, for example, that all methods containing the word “awesome” should be printed out just like in Example 9. All other methods should raise the default NoMethodError.

# Example 10
class MyGhostClass
  def method_missing(name, *args)
    if name.to_s =~ /awesome/
      puts "#{name} was called with arguments: #{args.join(',')}"
    else
      super
    end
  end
end

m = MyGhostClass.new
m.awesome_method("one", "two") # =>  awesome_method was called with arguments: one,two
m.another_method("three", "four") # =>  NoMethodError

This time, calling awesome_method behaves just like in Example 9, but another_method doesn’t contain the word “awesome” so we pass this up the chain of inheritance so the default behaviour is not broken.

Ghost Methods

Strictly speaking, MyGhostClass#awesome_method is not really a method. If we create an instance of MyGhostClass and scan it’s methods for any with “awesome” in the name we won’t find any.

# Example 11
@m = MyGhostClass.new
@m.methods.grep(/awesome/) # => nil

Instead, we call this a ghost method. Ghost methods come with pros and cons. The major pro is the ability to write code that responds to methods when you have no way of knowing the names of those methods in advance. The major con is that changing Ruby’s default behaviour like this may cause unexpected bugs if you’re not careful with your method names. With that in mind, let’s go back to our CarModel example and see if we can extend the functionality a little further.

# Example 12
class CarModel
  def method_missing(name, *args)
    name = name.to_s
    super unless name =~ /(_info|_price)=?$/
    if name =~ (/=$/)
      instance_variable_set("@#{name.chop}", args.first)
    else
      instance_variable_get("@#{name}")
    end
  end
end

This example may look a little complex but is really quite simple. First, we take the name argument and convert it from a symbol to a string. Next, we say “send this method up the inheritance chain unless the name ends with _price, _price=, _info or _info=”. If the name ends in an equals sign then we know this is a setter method so we set an instance variable with the same name as our method (minus the =). If there’s no equals sign then we know this is a getter method and so we return the instance variable with the same name.

Now, we don’t have to specify the features each car model has in advance. We can simply get and set values on any _price or _info attribute during runtime:

# Example 13
@car_model = CarModel.new

@car_model.stereo_info    = "CD/MP3 Player"
@car_model.stereo_price   = "£79.99"

@car_model.stereo_info  # => "CD/MP3 Player"
@car_model.stereo_price # => "£79.99"

Conclusion

This tutorial has only scratched the surface of Ruby’s metaprogramming capabilities but hopefully it’s enough to spark your curiosity and will urge you to learn more about metaprogramming. The Ruby API covers all the methods I’ve talked about here plus more. For further reading, check out:

I hope you found this article valuable. Feel free to ask questions and give feedback in the comments section of this post. Thanks!

Do also read these awesome Guest Posts:

If you want to learn Ruby Metaprogramming in-depth, join our online course that starts 4th Dec. 2010. For details click here.

comments powered by Disqus