Metaprogramming

in Ruby

What is Metaprogramming?

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.

 

Or simply put, Metaprogramming is writing code that writes code during runtime.

But What does That Mean?

Let's take a look at this example below. Turn on your irb:

# First, let's create an instance of Object class
my_object = Object.new


# Next, we 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

# Then, we 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

# After the setups above, we can do this:
my_object.set_my_variable = "Hello"
my_object.get_my_variable

Did You See That?

In our previous example, we just added methods (getter and setter) to an object in runtime! After that we were able to invoke them on the object as if they were written beforehand.

Don't close your irb, now let's add this line:

# Now we create a new instance of Object class
my_other_object = Object.new

# Then, we try to invoke "set_my_variable=(var)" on it
my_other_object.set_my_variable = "Hello"

The code above will return "NoMethodError". That's because our "set_my_variable=" and "get_my_variable" methods are only defined for "my_object" object and therefore they are only available to "my_object".

Class Methods (1)

This is our standard way to define class method:

class MyClass
  def MyClass.capitalize_name
    name.upcase
  end
end

MyClass.capitalize_name
# will return "MYCLASS"

Class Methods (2)

Here is another way we can define a class method:

class MyClass
end

def MyClass.capitalize_name
  name.upcase
end

MyClass.capitalize_name
# this will return "MYCLASS"

In the example above, we define our class method outside of the definition of the class itself.

Class Methods (3)

Lastly, we can also define a class method this way:

MyClass = Class.new

def MyClass.capitalize_name
  name.upcase
end

MyClass.capitalize_name
# will return "MYCLASS"

In this last example, you should note that when we 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, MyClass is an instance of the "Class" class.

Dynamic Method Calls (1)

Take a look at this code:

class Glider
  def lift
    puts "Rising"
  end
  
  def bank
    puts "Turning"
  end
end

class Nomad
  def initialize(glider)
    @glider = glider
  end

  def do(action)
    if action == 'lift'
      @glider.lift
    elsif action == 'bank'
      @glider.bank
    else
      raise NoMethodError.new(action)
    end
  end
end

nomad = Nomad.new(Glider.new)
nomad.do("lift")
nomad.do("bank")

Dynamic Method Calls (2)

The solution in our previous code only works if we know exactly what methods available in the Glider class. Also, we need to create "elsif" clause for every methods available in the Glider class. Is there any way to make it more easy?

Dynamic Method Calls (3)

Of course there is. Ruby provides us with a "send" method. It works like this:

class Nomad
  def initialize(glider)
    @glider = glider
  end

  def do(action)
    @glider.send(action)
  end
end

Now our Nomad class can call methods from Glider class without having to know beforehand what methods available there.

Dynamic Method Calls (4)

We can also pass arguments to "send". This is an example:

class Nomad
  def initialize(glider)
    @glider = glider
  end

  def do(action, argument = nil)
    if argument == nil
      @glider.send(action)
    else
      @glider.send(action, argument)
    end
  end
end

Code That Writes Code (1)

Take a look at this class:

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

If we have taught you well, you will feel the itch to correct the code above. Do you know what is wrong and how to fix it?

Code That Writes Code (2)

Again, if we have taught you well, you will immediately think about adding attr_accessor for each instance variable in our Car class. This time, we will write our own attr_accessor method. Well, sort of. 

Code That Writes Code (3)

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

# Now we can execute:
car_model1 = CarModel.new
car_model1.engine_info = "Engine"
car_model1.engine_info

Code That Writes Code (4)

In our previous code, all features are stored as array. For each feature, we use Ruby's Module#define_method to, well, define several methods.

Congratulations! You have just written your first meta code!

The "attr_accessor" Method (1)

Our latest CarModel example can easily be replaced with attr_accessor though:

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

By now you may have guessed, "so, attr_accessor was meta?" If you're guessing this, Mr. Heisenberg has a few words for you.

The "attr_accessor" Method (2)

It turns out that we have been using a metaprogramming feature all along in attr_accessor. We thought it was a magical keyword that magically creates getter and setter.

In Ruby, attr_accessor is actually a Module#attr_accessor method defined as follow.

A Better "attr_accessor"

Using "attr_accessor" was good but not satisfying enough as we need to define two attr_accessor symbols for every CarModel's feature. Let's try this one:

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

# Now you can execute
car_model1 = CarModel.new
car_model1.engine_price = 100
car_model1.engine_price

Eloquent, isn't it?

Method Missing (1)

In Ruby, whenever you invoke 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.

Method Missing (2)

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 regular methods, 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.

Method Missing (3)

Now, let's try to build our own method_missing method.

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

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

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

# Now you can execute
car_model1 = CarModel.new
car_model1.wing_price = 100
# will return "wing_price= was called with arguments: 100"

Method Missing (4)

We can even create a "ghost method".

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

With a ghost method like in the example above, our CarModel class can have any methods named ending with "_info" or "_price" without having to define the methods beforehand.

Method Missing (5)

Ghost methods, however, 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.

Materials:

Ruby:

1. Code Academy

2. Ruby Monk

3. Ruby Koans

 

Rails:

1. Agile Web Development with Rails

2. Everyday Rails Testing with RSpec

Metaprogramming in Ruby

By qblfrb

Metaprogramming in Ruby

  • 332