metaprogramming

warning: Creating default object from empty value in /home3/ideaquar/public_html/hugoestrada/modules/taxonomy/taxonomy.pages.inc on line 33.

Ruby: first adventures into metaprogramming

While working on a personal project, I ran into the following case. I wanted method load() to take a hash with values. If a key in the hash exists as an instance method, then assign the value from the input hash to it. I could have coded around it using an array with the names of instance variables and working from there, but, where is the fun in that? Besides, I knew that I could use this as a concrete meta programming exercise for myself.

1. I made sure that accessors create an instance method
I started irb and tested whether a class with an accessor would create an instance variable automatically. I tried the following:

class A
 attr_accessor x
end
y = A.new
y.instance_variable_set "@x", "wow"
puts y.x # => "wow"

So far, it seems like it works! So back to the text editor, and I wrote the method.

2. Something isn't right

def load(data = nil)
   if data.instance_of? Hash
     data.keys.each do |key|
      if self.instance_variables.include? key
        attr_name = "@#{key}"
        self.instance_variable_set attr_name , data[key]
      end
     end
   end
 end

So I write the code above. I expect that my tests would pass. But they didn't. The if clause was never true, so there was never an assignment. What? I looked back at the class description. The accessor names were there. What was going on? After a few minutes, I remembered a lunch discussion that I listened to at Ruby Nation. It was discussed that ruby starts with an empty object, and it builds members and methods as they are declared or needed. To begin with, there isn't anything in an object beyond the Object methods themselves, or so I understood. So, that means that until there is any assignment to a member accessor, the instance variable that the accessor provides the getter and setter for method doesn't exist yet.

Since the instance variable wasn't there, I decided to check for whether the object could respond to its getter/setter method.

3. Getting it to work

def load(data = nil)
   if data.instance_of? Hash
     data.keys.each do |key|
      if self.respond_to? key
        attr_name = "@#{key}"
        self.instance_variable_set attr_name , data[key]
      end
     end
   end
 end

the difference happens on line 4. It went from

 if self.instance_variables.include? key
where I was checking for the existence of the instance variable (which doesn't exist until assignment) to
 if self.respond_to? key
which checks for whether the object would respond to the method. This fix the problem.

Syndicate content