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.
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
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".
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"
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.
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.
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")
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?
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.
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
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?
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.
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
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!
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.
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.
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?
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.
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.
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"
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.
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.
Ruby:
1. Code Academy
2. Ruby Monk
3. Ruby Koans
Rails:
1. Agile Web Development with Rails
2. Everyday Rails Testing with RSpec