Wednesday, September 09, 2009

Fun With Metaprogramming: hash_initializer()

Earlier this week I was doing a code review for a candidate for ThoughtWorks. One of the bits I ran across was an object constructor that looked something like this:

class MyClass
def initialize(name, price, is_cool, is_neato, is_rad)
@name = name
# et cetera
end
end

Not so bad, really, when you're looking right at the signature for the constructor. But when it's called from another file...

MyClass.new("bob", "1.00", true, false, true)
MyClass.new("fred", "2.00", false, true, true)
# et cetera

... the purpose of the first two parameters may be easy to infer, but the last three? Which order were those descriptors in anyway? To really be sure, one has to keep referring to the class definition. In my review, I opined that perhaps the constructor should take a single argument -- a hash with keys named after the objects instance variables and their corresponding values. Thusly:

class MyClass
def initialize(args)
args ||= {} # in case MyClass#new is called without any args
@name = args[:name] || ""
# et cetera
end
end

This frees things up a bit, and allows for parameters to be specified in any order, with reasonable defaults for any that might be left out. It also makes calls to the constructor a little more self-documenting.

But what would really be nice would be not having to write out this boilerplate code for everything that we want this kind of constructor for.

Enter metaprogramming, in the form of hash_initializer -- a nifty little class extension I found in Brian Guthrie's Awsymandias library (as used at my current ThoughtWorks gig). Behold:

1. module ClassExtension
2.
3. if !Class.respond_to?("hash_initializer")
4. def hash_initializer(*attribute_names, &block)
5. define_method(:initialize) do |*args|
6. data = args.first || {}
7. data.symbolize_keys!
8. attribute_names.each do |attribute_name|
9. instance_variable_set "@#{attribute_name}", data[attribute_name]
10. end
11. instance_eval &block if block
12. end
13. end
14. end
15.
16. end
17.
18. Class.send :include, ClassExtension

Lines 5 through 12 correspond pretty much to my second version of MyClass#initialize above. Line 6 does the same defensive guard against a nil first parameter. Lines 8 through 10 pull each attribute, create an instance variable with the same name, and set it to the desired value. The extra bits? Line 7 does something I should have done in my "improved" version -- it forces the keys of the params has to be symbols, avoiding the awkwardness of blowing up when a key is a String when it should be a symbol or vice versa. Line 11 allows for the execution of a block along with initialization, if you're into that sort of thing. And the rest just opens up the class Class and makes Class#hash_initializer available to all objects.

With Class thus extended, the boilerplate for MyClass becomes simply:

class MyClass
hash_initializer :name, :price, :is_cool, :is_neato, :is_rad
end

And instantiating a MyClass stays what you would expect:

MyClass.new :name => "bob", :price => "1.00" # et cetera.

I like it.

irb(main):001:0> Class#hash_initializer.is_neato?
=> true