Hijacking a Pejorative: Monkey Patching and Technorati-Ruby

December 15, 2006

Monkey Patch

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.