Sunday, July 15, 2007

Ruby: Metaprogrammatically defining methods

Neal Ford and I were recently looking at some code that used metaprogramming to define similar methods. The example below isn't the code, nor would I suggest managing state in this way; however, it does provide an example of metaprogrammatically defined methods.

class Ticket
attr_accessor :status

[:planned, :pre_sale, :on_sale].each do |method_name|
define_method :"#{method_name}?" do
status == method_name
end
end
end

The above code works, but I find it painful to maintain. When I begin to read [:planned, :pre_sale, :on_sale].each do |method_name| I'm not expecting to find methods being defined. Often, I define class methods that give me a more descriptive way to generate the similar methods. In this case I would consider creating a create_status_boolean_methods method. If I went down that path the code could look like the example found below.

class Ticket
attr_accessor :status

def self.create_status_boolean_methods(*methods)
methods.each do |method_name|
define_method :"#{method_name}?" do
status == method_name
end
end
end

create_status_boolean_methods :planned, :pre_sale, :on_sale
end

This solution is decent, but there are a few things I don't like about it. Firstly, it's usually not important to understand how the create_status_boolean_methods method does it's job. Second, the definition of create_status_boolean_methods is often not near the actual usage of create_status_boolean_methods; therefore, on the rare occasion that you do need to understand how the methods are being defined you'll have to do a little searching within the class. Third, and perhaps the most obvious, it would take less code to define each method on it's own.

The above option is a good solution in some cases; however, Neal and I were looking for something simpler that didn't suffer from the above list of limitations. We came up with the following generalized solution to defining similar methods.

class Class
def def_each(*method_names, &block)
method_names.each do |method_name|
define_method method_name do
instance_exec method_name, &block
end
end
end
end

Given the def_each method defined on Class the Ticket class becomes easier to read in my opinion. The reason it's easier (in my opinion) is because when I'm scanning the file and I see def_each :planned?, :pre_sale?, :on_sale? do |method_name| I know that similar methods are being defined. Below is the full code now required to define the Ticket class.

class Ticket
attr_accessor :status

def_each :planned?, :pre_sale?, :on_sale? do |method_name|
:"#{status}?" == method_name
end
end

You may have noticed the instance_exec method call in the implementation of the def_each method. I've previously written about instance_exec, but I'll include the implementation here for completeness.

class Object
module InstanceExecHelper; end
include InstanceExecHelper
def instance_exec(*args, &block)
begin
old_critical, Thread.critical = Thread.critical, true
n = 0
n += 1 while respond_to?(mname="__instance_exec#{n}")
InstanceExecHelper.module_eval{ define_method(mname, &block) }
ensure
Thread.critical = old_critical
end
begin
ret = send(mname, *args)
ensure
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
end
ret
end
end
Post a Comment