CSC/ECE 517 Fall 2007/wiki1b 4 pm
Introduction
Metaprogramming refers to the writing of programs that generates code at run time. Naturally, this is lucrative, as a programmer can direct his efforts towards developing other code in the time that it would to take to generate this code. Metaprogramming allows us to dynamically add behavior to existing classes and objects.
Ruby is useful for Metaprogramming as it is dynamic and reflective. It allows flexibility in writing new control structures.
Uses of Metaprogramming
- Programs written in a language such as Ruby require less code to be written. Therefore, it takes to develop such programs and consequently, less time to market them.
- If programs are written with less code, they will eventually have fewer bugs.
- The programs are also easier to maintain, taking less time to develop new features, leading ultimately to greater user satisfaction.
Problem Definition
Our problem states: “There are many good examples of metaprogramming in Ruby on the Web. Take a look at, say, a dozen of them, and write a guide to them. Classify them into whatever categories are appropriate, and give recommendations on how to proceed through them to acquire a good knowledge of the uses and usefulness of metaprogramming.”
Implementing Metaprogramming
We’ve explored different ways of implementing metaprogramming in Ruby. This article is a guide to learning Metaprogramming by example.
There are several methods to implement Metaprogramming. These can be classified into the following categories:
- eval, class_eval, module_eval, instance_eval
- define_method
- proc
- instance_variable_set and instance_variable_get
Metaprogramming with eval, class_eval, module_eval, instance_eval
Here are a few examples using evals. Ruby has several evals: eval, class_eval, module_eval, instance_eval.
Using eval
In this example, MagicLamp creates an instance method only when the class method remember_incantation is called. The instance method corresponds to the incantation that the MagicLamp was told to remember.
class MagicLamp
def self.remember_incantation(incantation)
eval "def #{incantation}; puts '#{incantation}!'; end"
end
end
lamp = MagicLamp.new
lamp.respond_to? :kazaam #false
#This tests if the object will respond to a method call “kazaam”
#This code produces “FALSE” since kazaam doesn’t exist
MagicLamp.remember_incantation "kazaam"
lamp.respond_to? :kazaam # true
lamp.kazaam # "kazaam!"
Using class_eval
Let’s say we need to define an attribute for a class:
class MyClass attr_accessor :id, :diagram, :telegram end
This code can be refactored to accept attribute names as arguments rather than specifying them (using metaprogramming) as follows:
class Class
def my_attr_accessor( *args )
args.each do |name|
self.class_eval do
attr_accessor :"#{name}"
end
end
end
end
class MyNewClass my_attr_accessor :id, :diagram, :telegram end
The use of metaprogramming is illustrated above in they way the attributes are created; ‘my_attr_accessor’ creates an attribute by iterating over the arguments passed to it.
Using module_eval
Sometimes, we need to define instance and class methods of a class at runtime, when you are outside the class. module_eval helps do just that.
Example: Defining an instance method
class C
end
C.module_eval do
define_method :wish do
p "hello instance method"
end
end
c = C.new
c.wish #hello instance method
Example: Defining a class method
class C
end
C.module_eval do
class << self
define_method :wish do
p "hello class method"
end
end
end
C.wish #hello class method
Example: Another form of using module_eval when method body is available as a String object
class D
class << self
def method_body
ret =<<-EOS
def wish
p "hello, supplied as String object"
end
EOS
end
end
class C
end
c = C.new
c.class.module_eval(D.method_body)
c.wish # hello, supplied as String object
end
Using instance_eval
The instance_eval method of Object allows you to evaluate a string or block in the context of an instance of a class. One can create a block of code in any context and evaluate it later in the context of an individual instance. In order to set the context, the variable self is set to the instance while the code is executing, giving the code access to the instance's variables.
class Navigator
def initialize
@page_index = 0
end
def next
@page_index += 1
end
end
navigator = Navigator.new
navigator.next
navigator.next
navigator.instance_eval "@page_index" #=> 2
navigator.instance_eval { @page_index } #=> 2
Metaprogramming with define_method
Using define_method
We can dynamically create methods at run-time, using define_method as follows:
class A
def fred
puts "In Fred"
end
def create_method(name, &block)
self.class.send(:define_method, name, &block)
end
define_method(:wilma)
puts "Charge it!"
end
class B < A
define_method(:barney, instance_method(:fred))
end
a = B.new
a.barney #In Fred
a.wilma #Charge it
a.create_method(:betty) { p self }
a.betty #B:0x401b39e8
The above code illustrates how to use metaprogramming by dynamically creating methods using define method. There are two ways of using define method:
define_method(symbol, method)
define_method(symbol) { block }
The following example shows another implementation of metaprogramming in which the define method gives us a way to bind the attributes of a method to the created methods, thereby changing its parameters.
MyCounter = Class.new
shared_count = 0 # A new local variable.
MyCounter.send :define_method, :double_count do
shared_count += 1
@count ||= 0
@count += 1
[shared_count, @count]
end
first_counter = MyCounter.new
second_counter = MyCounter.new
assert_equal [1, 1], first_counter.double_count
assert_equal [2, 2], first_counter.double_count
assert_equal [3, 1], second_counter.double_count
assert_equal [4, 2], second_counter.double_count
As can be seen, even if the method that defined the local variable shared_count completed execution, the method in MyCounter will still be bound to the context of the method.
class C
def wish
p "hello"
end
end
c = C.new
c.wish # hello
class D
class << self
def keep_some_record
p "I am keeping some records"
end
end
end
# aliasing the wish method
c.class.module_eval do
alias_method :wish_orig, :wish
define_method :wish do
D.keep_some_record
wish_orig
end
end
c.wish # I am keeping some records; hello
Metaprogramming with proc
Using proc
def create_proc(&p); p; end create_proc do puts "hello" end p.call(*args)
If you want to use the proc for defining methods, you should use lambda to create it, so return and break will behave the way you expect:
p = lambda { puts "hoho"; return 1 }
define_method(:a, &p)
Metaprogramming with instance_variable_set/ instance_variable_get
Adding fields based on need for them
Sometimes, one might not know what fields a class requires while defining the class. Ruby's Metaprogramming circumvents that problem, as illustrated below.
class BinaryTree
def add(value)
if @root.nil?
@root = BinaryTreeNode.new(value)
else
@root.add(value)
end
end
end
class BinaryTreeNode
def initialize(value)
@value = value
end
def add(value)
if value < @value
if @left.nil?
@left = BinaryTreeNode.new(value)
else
@left.add(value)
end
else
if @right.nil?
@right = BinaryTreeNode.new(value)
else
@right.add(value)
end
end
end
end
We see that there are many calls to create new objects of BinaryTree and BinaryTreeNode. Metaprogramming can help replace this code snippet:
if field.nil? field = BinaryTreeNode.new(value) else field.add(value) end
With:
module BinaryTreeHelper
private
def add_or_create_node(field, value)
if instance_variable_get(field).nil?
instance_variable_set(field,
BinaryTreeNode.new(value))
else
instance_variable_get(field).add(value)
end
end
end
And the classes can be changed accordingly as:
class BinaryTree
include BinaryTreeHelper
def add(value)
add_or_create_node(:@root, value)
end
end
class BinaryTreeNode
include BinaryTreeHelper
def initialize(value)
@value = value
end
def add(value)
add_or_create_node(value < @value ? :@left : :@right,
value)
end
end
This example is very illustrative in demonstrating how we can generate entities at runtime.
See Also
References
Defining attributes with Metaprogramming