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.
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
unless self.respond_to?(k.to_sym)
exists to ensure that hash_accessors
will not accidentally overwrite an accessor for which I have done something special -- and I do have one or two of those in the class.
reader_code = Proc.new { @personalia[k] }
exists because, while define_method
is supposed to be able to take a block, it does not appear to work that way when using send
to send the define_method
message. The same applies to writer_code = Proc.new {|new_value| @personalia[k] = new_value }
later on.
self.class.send :define_method, k.to_sym, reader_code
is the money shot for setting a getter, and also the most hackish looking part of all this, to me, along with its setter sibling self.class.send :define_method, "#{k}=".to_sym, writer_code
. It basically just uses define_method
to define a method with a name that matches the hash key and whose code is what's in the reader_code
or writer_code
proc.
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.
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.