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

3 comments:

  1. This article is a keeper. Great summary.

    For the particular example, I agree that the last is nicest.

    ReplyDelete
  2. Jay, some of the underscores seem to be escaping from your code when being formatted for colour in the article.

    You'll have to tighten up security :)

    ReplyDelete
  3. My favorite part of this idea is that it would make it easy to solve a really tricky problem: having such methods show up in rdocs. It wouldn't be too hard to modify rdoc to recognize the def_each method and, in many of the cases where it would be used, have the methods show up properly in rdocs. For example, Rails could have proper documentation of the get, post, put, delete, etc. methods in ActionController::TestProcess.

    ReplyDelete

Note: Only a member of this blog may post a comment.