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.