Toying with the dynamic features of Ruby : Adding methods at run time

January 14, 2007

 

I feel the way to start this blog is to introduce a trick that is central to RTracker, namely the possibility given to the Ruby programer to add methods to its classes on the flight. Like almost everything else you can wish for, Ruby provides the user with a method that does just this : Module#define_method. There is, however, a problem : that method is private. Don’t worry, there is a hack to do this anyway:

self.class.send(:define_method, “method_name”) { method_code }

That is, one has to send a message calling define_method to the class of the current object. In order to illustrate this, let’s write a small profiler that shows the performances of a method call. Note that the purpose of this program is to demonstrate how Ruby works, as Ruby already provides a very interesting tool, the benchmark.

If one wants to measure the time taken by a method to run, the simplest thing to do is to use the method Time#now, which returns what time it is. By calling this method before and after executing the “profiled” method, one can infer how much time the method took.

The next step is to replace the method whose performances we want to measure by our own method, that captures the current time, run the actual method and then captures the time again in order to display the difference. Why replace it, instead of simply creating a new method? Simply because profiling is interesting when dealing with huge pieces of code, and that one cannot imagine replacing every occurence of a widely used method. The following code does that for us:

class Object
def Object.profile symbol
rename_symbol = (”rprof_” + symbol.to_s).to_sym
alias_method rename_symbol, symbol
# Define the new method so as to add timing
# code.

self
.send(:define_method, symbol.to_s) { |*args|
start_time = Time.nowself.send(rename_symbol, *args)
puts (Time.now - start_time).to_s + ” have elapsed”
}
puts “The new method ” + rename_symbol.to_s + ” has been created for method ” + symbol
end
end

Then, in order to monitor a method, you can just use it like:

class Test
def hello
puts “hello”
end
profile :hello
end

Calling hello on an instance of set gives you the timing for that call.

There is something I have implicitely assumed, and haven’t talked about: the fact that the method is a member of class Module and not of class Class. Yet, we managed to add a method to our class. This is due to the fact that the module Kernel is automatically included in class Object. We have therefore access to that method at class level.

Another problem is that we do not take into account the fact that the Time#now method itself takes some time. Of course, one can imagine writing something like:

@@time_overhead = Time.now - Time.now

and then turning the time calculation code into:

puts (Time.now - start_time - @@time_overhead).to_s + " seconds have elapsed."

But that is not really relevant as trying Time.now - Time.now several times in irb returns very different values. That is because of the fact that it is run on a multitasked operating system, and that it is not possible to ensure that there will always be the same amount of time between the two calls of Time#now. Which means that measures are relevant when their order is bigger than the order of the scheduler’s overhead. When the order is not so different, only the average over several values is relevant.

A fix to this problem could be to focus only on how much user time has elapsed. Which is beyond the scope of this post.

About these ads

4 Responses to “Toying with the dynamic features of Ruby : Adding methods at run time”

  1. Martin Vahi said

    I had some problems with the visibility of the new methods that were acuired by the define_method “approach”. So, here’s a hack that overcomes that problem. It can be copy-pasted into a class code and I place it into public domain. :)

    private
    # Adds a metohd to self.
    def add_method(method_name, class_of_origin)
    if not defined? @new_methods_at_self
    @new_methods_at_self=Array.new
    end # if
    new_method=”variable new_method will change type”
    eval “new_method=class_of_origin.method(:” + method_name + “)”
    new_method_args=””
    new_method.arity.times {|i| new_method_args+=”x” + i.to_s + “, “}
    if 0

  2. Martin Vahi said

    Uups. I’m sorry. I didn’t know that source can not be posted here.

  3. Thanks for this explanation. I’ve been looking low and high for a concise explanation of this technique and, thanks to this site, I’m now using it!

  4. Random T. said

    Not that I’m impressed a lot, but this is a lot more than I expected when I stumpled upon a link on Digg telling that the info is quite decent. Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: