aboutsummaryrefslogtreecommitdiff
path: root/content/posts/2006-12-15-hijacking-a-pejorative-monkey-patching-and-technorati-ruby.html
blob: af40a6f728899eadf5d990a0b3e07311811b32a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
---
date: "2006-12-15T06:34:07Z"
title: 'Hijacking a Pejorative: Monkey Patching and Technorati-Ruby'
---

<p></p>

<p><img src='http://pablotron.org/files/monkey_patch.jpg' 
  width='200' height='163' title='Monkey Patch' alt='Monkey Patch'
  align='right' border='0' style='padding: 5px;' /></p>

<p>The new version of <a href="http://pablotron.org/software/technorati-ruby/">Technorati-Ruby</a> adds a bit of magic to return
values.  Version 0.1.0 returns standard <a href="http://ruby-lang.org/">Ruby</a> hashes.  A list of
items in the returned value -- blogs from <code>Technorati#cosmos</code> or tags
from <code>Technorati#tag</code>, for example -- are returned as an array of hashes
under the the <code>'items'</code> key, like so:</p>

<pre><code># 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']]
}
</code></pre>

<p>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.</p>

<p>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 <em>monkey patching</em>.</p>

<p><strong>Monkey Patching</strong><br/>
What is monkey patching, anyway?  <a href="http://en.wikipedia.org/wiki/Monkey_patch">Wikipedia defines it</a> as "a
way to extend or modify runtime code without altering the original
source code for dynamic languages".  If you're a <a href="http://rubyonrails.com/">Rails</a> user, you've
already been merrily enjoying the benefits of monkey patching:</p>

<pre><code>&gt;&gt; strs = %w{monkey patch}
?&gt; strs.map(&amp;:pluralize)
=&gt; ["monkeys", "patches"]`
</code></pre>

<p>(Hint: neither <code>String#pluralize</code> nor the no block/one-argument form of
<code>Enumerable#map</code> exist in the standard library; both are grafted on at
run-time by ActiveSupport)</p>

<p>Anyway, the <a href="http://python.org/">Python</a> community <a href="http://blog.ianbicking.org/theres-so-much-more-than-rails.html">frowns on the practice</a>.  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.  <a href="http://chadfowler.com/">Chad's</a> post, <a href="http://chadfowler.com/index.cgi/Computing/Programming/Ruby/TheVirtuesOfMonkeyPatching.rdoc,v">"The Virtues of
Monkey Patching"</a>, 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:</p>

<blockquote>
  <p><strong>Paul's Rule of Monkey Patching</strong><br/>
  <em>Libraries should not modify underlying classes at runtime unless that
  is their express purpose and applications should ignore what I just
  said.</em></p>
</blockquote>

<p>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 <code>Hash</code>
either.  Fortunately, I had another option: <a href="http://project.ioni.st/post/966#post-966">just in time convenience
methods</a>, the sneaky and verbosely-named cousin of monkey
patching.  </p>

<p><strong>Just in Time Convenience Methods</strong><br/>
A <em>just in time convenience method</em> is a convenience method that is
added to an <em>instance</em> of a class, rather than the class itself.
<a href="http://weblog.jamisbuck.org/2006/11/15/mini-api-s">Jamis</a> and <a href="http://project.ioni.st/post/966#post-966">Marcel</a> both have more to say about them, but here's
what they look like:</p>

<pre><code>nog_str = 'delicious egg nog'

def nog_str.de_nog
  gsub!(/egg nog/i, 'apple juice')
end

nog_str.class
=&gt; String
nog_str.de_nog
=&gt; "delicious apple juice"
</code></pre>

<p>As you can see, <code>nog_str</code> is still a <code>String</code>, just with a little more
personality.  You can also use <code>Object.instance_eval</code>:</p>

<pre><code>class A
  private
  def secret
    'secret message'
  end
end

&gt;&gt; a = A.new
&gt;&gt; a.secret
NoMethodError: private method `secret' called for #&lt;A:0xb78b9fb8&gt;
      from (irb):40
&gt;&gt; a.instance_eval { secret }
=&gt; "secret message"
</code></pre>

<p>Which brings us back to Technorati-Ruby.  First, I added a bit of code
to jazz up result hashes with convenience methods:</p>

<pre><code>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
</code></pre>

<p>Second, I wrapped the result hashes in <code>magify_hash</code>.  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:</p>

<pre><code># 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] 
}
</code></pre>

<p>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 <code>Hash</code>, and they allow users to write cleaner
code.  Not bad for a sneaky pejorative.</p>