Saturday, September 12, 2009

Ruby metaprogramming toolbox

An article that lists and explains all the ruby metaprogramming techniniques.

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

Getting, setting, and destroying constants (like classes)

Adding and removing methods

Running dynamic code

Reflection methods

Reflection is an important part of metaprogramming as it allows us to look at objects and peer at their contents and structure.

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:

Blog Archive