Archive for October 15th, 2007

I am giving a Ruby lecture tomorrow (some students happen to be quite lucky in fact). I wanted to present an example of a Ruby On Rails style meta programming technique. For example you can have a find_by_name or find_by_email method on your ActiveRecord objects. Those methods are in fact generated on the fly using a technique which looks like what follows. This has probably been explained a million times somewhere else on the web, but I don’t care ;-)

So let’s start with a very basic contacts book class which looks like:

class ContactBook
  def initialize
    @contacts = Hash.new
  end
 
  def add_contact(name, email)
    @contacts[name] = email
  end
 
  def contact_named(name)
    @contacts[name]
  end
end
 
book = ContactBook.new
book.add_contact "Pierre", "pierre@zzland.fr"
book.add_contact "Julien",  "julien@zzland.fr"
book.add_contact "Yoan",   "yoan@zzland.fr"
book.add_contact "Mr Bean", "info@mrbean.com"

Nothing impressive here, so now let’s get all the contacts whose email contain a given substring:

class ContactBook
  def email_containing(str)
    @contacts.select { |key, value| value.include? str }
  end
end
 
def display_contact(contact)
  puts "#{contact[0]} <#{contact[1]}>"
end
 
zzland = book.email_containing "zzland"
zzland.each { |contact| display_contact(contact) }

But what if we could have email_thestringtolookfor methods instead of this? Let’s do it by evaluating some code on the fly:

class ContactBook
  def method_missing(id, *args)
    method_name = id.to_s
    if method_name[0..5] == 'email_'
      str = method_name[6..method_name.length]
      to_eval = <<-END_FUNC
        def email_#{str}
          @contacts.select { |key, value| value.include? "#{str}" }
        end
      END_FUNC
      instance_eval to_eval
      return eval("self.email_#{str}")
    end
  end
end
 
zzland = book.email_zzland
bean  = book.email_bean
zzland.each { |contact| display_contact(contact) }
puts "-----"
bean.each { |contact| display_contact(contact) }
  1. method_missing is invoked whenever a message is sent to the class instance, but no matching method name can be found.
  2. If the name starts with email_, then we are in luck! (note that I should throw an exception if the name doesn’t start with this to comply with the Ruby semantics, but I was too lazy for that)
  3. We generate a new method for the instance through evaluation.
  4. We do not forget to invoke the new method through evaluation, as the original call would return nil since the method had not been found.

Nice isn’t it? :-)

Comments 2 Comments »