http://weare.buildingsky.net/2009/08/25/rubys-metaprogramming-toolbox
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. 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. (via Wikipedia)
The following tutorial lists all the methods from the Ruby core that are useful for metaprogramming as well as demonstrates common usage scenarios you will find helpful to get started. In conclution, an example is presented showing how to develop a dynamic database class like ActiveRecord which automatically generates classes for database tables and populates each model class with getters and setters for its fields.
The Metaprogramming Tool Box
Ruby gives us many methods that help us generate code dynamically. Its important to familiarize yourself with them.
Getting, setting, and destroying variables
- Object#instance_variable_get
- Object#instance_variable_set
- Object#remove_instance_variable
- Module#class_variable_get
- Module#class_variable_set
- Module#remove_class_variable
Getting, setting, and destroying constants (like classes)
Adding and removing methods
Running dynamic code
- Object#send
- Object#instance_eval
- Module#module_eval (synonymous with Module#class_eval)
- Kernel#eval
- Kernel#method_missing
Reflection methods
Reflection is an important part of metaprogramming as it allows us to look at objects and peer at their contents and structure.
- Object#class
- Object#instance_variables
- Object#methods
- Object#private_methods
- Object#public_methods
- Object#singleton_methods
- Module#class_variables
- Module#constants
- Module#included_modules
- Module#instance_methods
- Module#name
- Module#private_instance_methods
- Module#protected_instance_methods
- Module#public_instance_methods
Evaluating strings and blocks
You may be familiar with the eval
method which allows you to evaluate a string or block as ruby code. When you need to eval within the scope of a particular object you can use the instance_eval
and module_eval
(synonymous for class_eval) methods.
The instance_eval
method works on the scope of an instantiated object.
[1,2,3,4].instance_eval('size') # returns 4
In the above example we passed instance_eval
the string ‘size’ which is interpreted against the receiving object. It is equivalent to writing:
[1,2,3,4].size
You can also pass instance_eval
a block.
# Get the average of an array of integers
[1,2,3,4].instance_eval { inject(:+) / size.to_f } # returns 2.5
Notice how the inject(:+)
and size.to_f
methods just float in the air with no receiving objects? Well because they are executed within the instance context they are evaluated as self.inject(:+) / self.size.to_f
where self is the receiving object array.
Whereas instance_eval
evaluates code against an instantiated object module_eval
evals code against a Module or Class.
Fixnum.module_eval do
def to_word
if (0..3).include? self
['none', 'one', 'a couple', 'a few'][self]
elsif self > 3
'many'
elsif self < 0
'negative'
end
end
end
1.to_word # returns 'one'
2.to_word # returns 'a couple'
We can see how module_eval
re-opened the existing class Fixnum and appended a new method. Now this in itself is nothing special as there are other ways to do this for instance:
class Fixnum
def to_word
..
end
end
The real advantage is to evaluate strings that generate dynamic code. Here we are using a class method create_multiplier
to dynamically generate a method with a name of your choosing.
class Fixnum
def self.create_multiplier(name, num)
module_eval "def #{name}; self * #{num}; end"
end
end
Fixnum.create_multiplier('multiply_by_pi', Math::PI)
4.multiply_by_pi # returns 12.5663706143592
The above example creates a class method (or ‘singleton method’) which when called, generates instance methods which any Fixnum object can use.
Using send
Using send
works much like instance_eval
in that it sends a method name to a receiving object. It is useful when you are dynamically getting a method name to call from a string or symbol.
method_name = 'size'
[1,2,3,4].send(method_name) # returns 4
You can specify the method name as a string or a symbol ‘size’ or :size
One potential benefit of send
is that it bypasses method access control and can be used to run private methods like Module#define_method
.
Array.define_method(:ducky) { puts 'ducky' }
# NoMethodError: private method `define_method' called for Array:Class
Using the send
hack:
Array.send(:define_method, :ducky) { puts 'ducky' }
Defining Methods
As we just saw in the example above we can use define_method
to add methods to classes.
class Array
define_method(:multiply) do |arg|
collect{|i| i * arg}
end
end
[1,2,3,4].multiply(16) # returns [16, 32, 48, 64]
method_missing
When included in a class, method_missing
is invoked when the class instance receives a method that does not exist. It can be used to catch these missing methods instead of raising a NoMethodError.
class Fixnum
def method_missing(meth)
method_name = meth.id2name
if method_name =~ /^multiply_by_(\d+)$/
self * $1.to_i
else
raise NoMethodError, "undefined method `#{method_name}' for #{self}:#{self.class}"
end
end
end
16.multiply_by_64 # returns 1024
16.multiply_by_x # NoMethodError
How does attr_accessor
work?
Most of us use attr_accessor
in our classes, but not everyone understands what it does behind the scenes. attr_accessor
dynamically generates a getter and a setter for an instance variable. Lets take a closer look.
class Person
attr_accessor :first_name
end
john = Person.new
john.first_name = 'John'
john.instance_variables
# returns ["@first_name"]
john.methods.grep /first_name/
# returns ["first_name", "first_name="]
We can see that attr_accessor
actually created an instance variable @first_name as well as two instance methods, a getter and a setter, first_name
and first_name=
Implementation
All classes inherit class methods from Module so we will put the mock methods here.
class Module
# First using define_method
def attr1(symbol)
instance_var = ('@' + symbol.to_s)
define_method(symbol) { instance_variable_get(instance_var) }
define_method(symbol.to_s + "=") { |val| instance_variable_set(instance_var, val) }
end
# Second using module_eval
def attr2(symbol)
module_eval "def #{symbol}; @#{symbol}; end"
module_eval "def #{symbol}=(val); @#{symbol} = val; end"
end
end
class Person
attr1 :name
attr2 :phone
end
person = Person.new
person.name = 'John Smith'
person.phone = '555-2344'
person # returns
Both define_method
and module_eval
produced the same result.
Example Usage: Poor Man’s Active Record
For those familiar with RubyonRails it is easy to see how one might go about implementing an ActiveRecord class which would look up field names in a database and add getters and setters to a class.
We could take it one step further and have the Model classes generated dynamically as well.
In this example we are going to generate a poor man’s ActiveRecord. The class will connect to the MySQL database, generate a dynamic class for every table it finds, and populate the classes with getters and setters the table fields they contain.
require 'rubygems'
require 'mysql'
class PoorMan
# store list of generated classes in a class instance variable
class << self; attr_reader :generated_classes; end
@generated_classes = []
def initialize(attributes = nil)
if attributes
attributes.each_pair do |key, value|
instance_variable_set('@'+key, value)
end
end
end
def self.connect(host, user, password, database)
@@db = Mysql.new(host, user, password, database)
# go through the list of database tables and create classes for them
@@db.list_tables.each do |table_name|
class_name = table_name.split('_').collect { |word| word.capitalize }.join
# create new class for table with Module#const_set
@generated_classes << klass =" Object.const_set(class_name, Class.new(PoorMan))
klass.module_eval do
@@fields = []
@@table_name = table_name
def fields; @@fields; end
end
# go through the list of table fields and create getters and setters for them
@@db.list_fields(table_name).fetch_fields.each do |field|
# add getters and setters
klass.send :attr_accessor, field.name
# add field name to list
klass.module_eval { @@fields << field.name }
end
end
end
# finds row by id
def self.find(id)
result = @@db.query("select * from #{@@table_name} where id = #{id} limit 1")
attributes = result.fetch_hash
new(attributes) if attributes
end
# finds all rows
def self.all
result = @@db.query("select * from #{@@table_name}")
found = []
while(attributes = result.fetch_hash) do
found << new(attributes)
end
found
end
end
# connect PoorMan to your database, it will do the rest of the work for you
PoorMan::connect('host', 'user', 'password', 'database')
# print a list generated classes
p PoorMan::generated_classes
# find user with id:1
user = Users.find(1)
# find all users
Users.all
No comments:
Post a Comment