--- date: "2006-12-15T06:34:07Z" title: 'Hijacking a Pejorative: Monkey Patching and Technorati-Ruby' ---
The new version of Technorati-Ruby adds a bit of magic to return
values. Version 0.1.0 returns standard Ruby hashes. A list of
items in the returned value -- blogs from Technorati#cosmos
or tags
from Technorati#tag
, for example -- are returned as an array of hashes
under the the 'items'
key, like so:
# find sites linking that link to me
results = tr.cosmos('pablotron.org')
# print an excerpt from each item
puts results['items'].map { |item|
[item['url'], item['excerpt']]
}
It's a simple system, and using a hash instead of a pre-defined class reinforces the idea that the return values could be unavailable, change, or possibly even be removed. The problem, of course, is that the hash references in the example above clutter the code and cause it to look more like [Perl] than Ruby.
I wanted to give the results a bit more of a Ruby feel, preferrably without breaking backwards compatability. I came up with a solution that I'm pretty happy with. We'll get to that in a minute; first let's talk about monkey patching.
Monkey Patching
What is monkey patching, anyway? Wikipedia defines it as "a
way to extend or modify runtime code without altering the original
source code for dynamic languages". If you're a Rails user, you've
already been merrily enjoying the benefits of monkey patching:
>> strs = %w{monkey patch}
?> strs.map(&:pluralize)
=> ["monkeys", "patches"]`
(Hint: neither String#pluralize
nor the no block/one-argument form of
Enumerable#map
exist in the standard library; both are grafted on at
run-time by ActiveSupport)
Anyway, the Python community frowns on the practice. In fact, the term "monkey patch" comes from the Python community, and is actually meant as a pejorative. The Ruby community, on the other hand, is more tolerant of the practice. Chad's post, "The Virtues of Monkey Patching", is a fantastic real-world example of how monkey patching can be beneficial. When is monkey patching appropriate, and when should it be avoided? Here's my rule of thumb:
Paul's Rule of Monkey Patching
Libraries should not modify underlying classes at runtime unless that is their express purpose and applications should ignore what I just said.
How does monkey patching apply to Technorati-Ruby? Well, it doesn't, or
at least not directly. I didn't want to extend the standard library for
little old Technorati-Ruby, and I didn't really want to sub-class Hash
either. Fortunately, I had another option: just in time convenience
methods, the sneaky and verbosely-named cousin of monkey
patching.
Just in Time Convenience Methods
A just in time convenience method is a convenience method that is
added to an instance of a class, rather than the class itself.
Jamis and Marcel both have more to say about them, but here's
what they look like:
nog_str = 'delicious egg nog'
def nog_str.de_nog
gsub!(/egg nog/i, 'apple juice')
end
nog_str.class
=> String
nog_str.de_nog
=> "delicious apple juice"
As you can see, nog_str
is still a String
, just with a little more
personality. You can also use Object.instance_eval
:
class A
private
def secret
'secret message'
end
end
>> a = A.new
>> a.secret
NoMethodError: private method `secret' called for #<A:0xb78b9fb8>
from (irb):40
>> a.instance_eval { secret }
=> "secret message"
Which brings us back to Technorati-Ruby. First, I added a bit of code to jazz up result hashes with convenience methods:
def magify_hash(hash)
hash.keys.each do |key|
meth_key = key.gsub(/\//, '_')
hash.instance_eval %{def #{meth_key}; self['#{key}']; end}
end
hash
end
Second, I wrapped the result hashes in magify_hash
. Third? There
wasn't really a third step, so I just sat around for a few minutes
feeling smug. By the way, here's that example code from the beginning,
updated to use the shiny new convenience methods:
# find sites linking that link to me
results = tr.cosmos('pablotron.org')
# print an excerpt from each item
puts results.items.map { |item|
[item.url, item.excerpt]
}
So, problem solved. Just in time convenience methods satisfy all my
requirements: they're backwards compatible, don't require me to create a
new class or sub-class Hash
, and they allow users to write cleaner
code. Not bad for a sneaky pejorative.