CSC/ECE 517 Fall 2007/wiki1b 4 pm

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

Metaprogramming refers to the writing of programs that generates code at run time. It 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.

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. Here is a guide to learning Metaprogramming by example.

There are several methods to implement Metaprogramming. These can be classified into the following categories:

Metaprogramming with eval, class_eval, module_eval, instance_eval

Metaprogramming with define_method

Metaprogramming with proc

Metaprogramming with instance_variable_set/ 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.

Example using eval:

 class MagicLamp  
   def self.remember_incantation(incantation)  
     eval "def #{incantation}; puts '#{incantation}!'; end"  
   end  
 end  
 
 lamp = MagicLamp.new  
 lamp.respond_to? :kazaam	                 #false
  1. This tests if the object will respond to a method call “kazaam”
  2. This code produces “FALSE” since kazaam doesn’t exist
 MagicLamp.remember_incantation "kazaam"  
 lamp.respond_to? :kazaam			 # true  
 lamp.kazaam					 # "kazaam!"

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.

Example 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

Here, the use of metaprogramming is illustrated in they way the attributes are created; ‘my_attr_accessor’ creates an attribute by iterating over the arguments passed to it.

Examples using module_eval:

module_eval defines instance and class methods of a class at runtime, when you are outside the class.

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  

Example 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

Example using define_method

 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 } 

Example: 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.

 GuineaCounter = Class.new
     shared_count = 0 # A new local variable.
     GuineaCounter.send :define_method, :double_count do
       shared_count += 1
       @count ||= 0
       @count += 1
       [shared_count, @count]
     end
     first_counter = GuineaCounter.new 
 second_counter = GuineaCounter.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 GuineaCounter 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  
  1. 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

Example: Using proc

 def create_proc(&p); p; end
 create_proc do

puts "hello"

 end       #  #<Proc ...>
 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

Example: Adding fields based on need for them

 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.

REFERENCES

Metaprogramming [1] [2] [3] [4] [5] [6] [7] [8]