It was harder than I thought to write a useful method_missing class for Hash object, so I thought I’d share the fruit of my labours here.
class Hash
def method_missing(m, *args, &block)
method = m.to_s
is_assignment = method[-1,1] == '='
key = is_assignment ? method[0..-2] : method[0..-1]
self[key] = args.first if args && is_assignment
raise NoMethodError if !self[key]
return self[key]
end
end
The difficulty lies in detecting and acting correctly in situations where we want to assign a value to the hash. Annoyingly, we have to go and actually check for an =
which inexplicably gets attached to the end of the method if present.
Anyway, let’s see if it works:
>> hsh = {'ping' => 'pong'}
=> {"ping"=>"pong"}
>> hsh.ping
=> "pong"
>> hsh.ping2
NoMethodError: NoMethodError
from /path/initializers/hacks.rb:31:in 'method_missing'
from (irb):3
>> hsh.ping2 = "pong2"
=> "pong2"
>> hsh.ping2
=> "pong2"
>> hsh.keys
=> ["ping2", "ping"]
Yes, pretty basic, but I tried and failed to find anything that handled assignation that wasn’t a overblown nightmare or didn’t actually work so thought I’d share. As far as I can see this is the Simplest Thing That Can Possibly Work™ for this case, any improvements welcome.
Also note that we are standardising on hash keys being strings here. If you want to use that code with Symbol keys (popular in Rails), you’d have to extend it a bit. I had actually been under the impression that symbols and strings in hashes were going to be considered equal in Ruby 1.9 but apparently just in comparison, not access.
Another useful addition might be a hash of “acceptable” keys and their types as a class variable, which you’d feed with a DataMapper-style list of properties for the class. Check any inserts against that and you’ve got basic validations, sans the 20,000 lines of impenetrable black box magic of the popular mega-libraries.
One more thing: obviously I would not recommend actually putting this on Hash proper. Monkeypatching such a basic class is asking for trouble. Instead, create a new class descending from Hash, create your objects in that, and use it from there.
UPDATE: looks to be a decent library for doing something similar if you don’t trust my hacks or if you prefer a gem. Personally a gem has to do something pretty fucking special for me to require it these days but you might prefer otherwise. It’s too big for me and tries to do too much, most of which I don’t want. But, it’s a *lot* smaller than going for a full AR object or something.
On another note, something to bear in mind is that method_missing
is slow. Using it to look inside a hash is about 5 times slower than accessing the key directly. That probably doesn’t matter, but it might pay to think twice before putting a method_missing call inside a loop that’s going to run a lot.
1,000,00 lookups (in seconds):
string hash: 1.041939
symbol hash: 0.699153
method missing hash: 4.645116
UPDATE 2: I’m a sucker for writing these little mini-benchmarks. The above was this code:
def self.test
count = 0
hsh = {'ping' => "PONG"}
time = Time.now
1_000_000.times do
count += 1 if hsh['ping'] == "PONG"
end
puts "string hash: " + (Time.now - time).to_s
count = 0
hsh = {:ping => "PONG"}
time = Time.now
1_000_000.times do
count += 1 if hsh[:ping] == "PONG"
end
puts "symbol hash: " + (Time.now - time).to_s
count = 0
chsh = CouchHash.new # monkeypatched hash ; )
chsh['ping'] = "PONG"
time = Time.now
1_000_000.times do
count += 1 if chsh.ping == "PONG"
end
puts "method missing hash: " + (Time.now - time).to_s
end
Note the speed. Not *that* bad. I thought about posting the code, as I always do, but thought “guh, I’m not posting that ugly crap. I can clean that up” .. so did. But the only way I could think of to trigger the different lookup methods was via an eval()
, and what that did to the results is instructive.
def self.test
hsh = CouchHash.new
hsh['ping'] = hsh[:ping] = "PONG"
tests = ["['ping']", "[:ping]", ".ping"]
tests.each do |call|
time = Time.now
1_000_000.times do
raise if !(eval("hsh" + call) == "PONG")
end
puts call + ': ' + (Time.now - time).to_s
end
end
Wow, doesn’t that look better! Well, one thing doesn’t look better .. the results:
['ping']: 6.494104
[:ping]: 5.557214
.ping: 9.39655
Woah. The eval slowed us down by a factor of 10 in the case of the symbol lookup. Lesson: don’t use eval. Especially in combination with method_missing!!