blogstrapping

Metaprogrammatic Hash Accessors

I used to have a metric crapton of accessors, individually defined the old-fashioned way, in a Ruby class (that's an object oriented class, not a school class). I was not using attr_accessor and friends for this, because the accessors do not access discrete instance variables; they access the elements of a specific hash in the instance. I had been thinking about doing something to reduce the weight of repetitive code for this, but had not ever really gotten around to it for a while. By "something", of course, I mean "metaprogramming". Eventually -- meaning a day or two ago -- Bitbucket user captainjey took a look at the code and commented to me in IRC about how I should sprinkle some metaprogramming magic on it to get rid of all the crufty repetitiveness, and while I basically said "maybe later, it works for now and I have more important things to work on," it was not long before I felt inspired to tackle the problem.

I am now using metaprogramming to accomplish what I used to do with brute force: defining accessors based on hash keys rather than discrete instance variables. I feel like I'm doing something a bit hackish/kludgey with the syntax, specifically where using the send message for send_method, but I have not come up with a more elegant way to do it. Any suggestions are welcome. You can use the contact page on this site to get in touch with me with such suggestions.

Actual Working Code

class Persona
  attr_accessor :personalia

  def initialize(personalia=Hash.new)
    @personalia = personalia

    # skip a bunch of stuff

    hash_accessors
  end

  # skip a bunch of stuff

  def hash_accessors
    @personalia.keys.each do |k|
      unless self.respond_to?(k.to_sym)
        reader_code = Proc.new { @personalia[k] }
        self.class.send :define_method, k.to_sym, reader_code

        writer_code = Proc.new {|new_value| @personalia[k] = new_value }
        self.class.send :define_method, "#{k}=".to_sym, writer_code
      end
    end
  end

  # skip a bunch of stuff
end

Code Notes

Used Twice

I have actually used this technique in two different classes, in two different projects, this weekend. In both cases, accessors for the parent hashes of the keys I'm turning into method names already exist. For convenience purposes, though, I wanted the keys to become method names for accessors themselves. In the case not shown in the above example, I only use the technique to generate getters; there are no setters for the keys of that hash. Of course, in that case the hash in question (and stuff to manipulate it) is not pretty much the entire object instantiated from the class, as in the case in Persona.

Language Design and Metaprogramming

I have plans to delve more deeply into C than I ever have before, some time later this year (probably in a month or so). I expect that when I do so I will miss the metaprogramming facilities and dynamism of Ruby pretty bitterly. On the other hand, after doing some embedded work in C (which is kinda where I plan to go with it for a while, maybe starting with a low-end Arduino kit), I expect that I'll miss some of the capabilities of C when working with Ruby, too. It has been long enough since I have done anything with C right now that I rather don't miss it, because all I really remember is the amount of work that often has to be done to achieve things that are exceedingly simple in many other languages.

As I said above, I really feel like there must be a cleaner way to code this up. If there isn't, though, I have to wonder about the design decisions that led to this. It is entirely unstraightforward, in that it's the sort of thing that requires more knowledge of the fiddly bits of the language than I tend to feel such things should (yeah, and soon I'll be using C a lot more, where everything in the language is fiddly bits). I do not have enough experience with Common Lisp (precious little, in fact) to be able to judge Ruby by that benchmark, but from the way people talk about Lisp macros I expect things are probably a bit more eloquent in the realm of Lispy metaprogramming than this example might suggest about Ruby.