The Monkey, Refined

Monkey-patching. It strikes fear into newcomers to Ruby, excitement and abandon in moderate Ruby developers, and sad, disgruntled sighs from the experienced developers who end up having to wade through a huge Rails codebase to find out where somebody decided to monkey-patch the mysql2 gem in a way that makes it incompatible with a Rails 4 upgrade (bonus points for hiding the patch in something like app/models/stats/nightly_stats_run/customer_roi.rb, obviously).

(bitter tears of experience here)

But hark! Ruby 2.1 is here, and may contain some salvation in the form of refinements. First, though, a refresher.

Patch that Monkey!

Monkey-patching in Ruby involves modifying code at run-time, either by extending and providing additional functionality or redefining existing code. And Ruby makes it so easy! Consider this little gem:

class String
  def all_is_love
    ‘❤’ * self.length
  end
end

This extends String with the method :all_is_love, so:

pry(main)> 'hello'.all_is_love                                              
=> "❤❤❤❤❤”

This is how large chunks of the Rails helper methods (e.g. :camelize) are implemented. And this is fine…except due to the Magic of Ruby, you can do this pretty much anywhere in your code. And you can do this as well:

class String
  def length
    2
  end
end

pry(main)> 'hello’.length                                              
=> 2

Just to give you an idea of the systemic breakdown this can create, let’s go back to our earlier method:


pry(main)> 'hello'.all_is_love
=>"❤❤”
Again, there are sometimes reasons why you have to monkey-patch and replace methods at runtime. Perhaps your database connection has some legacy weirdness that needs to be worked around and it isn’t covered by a standard gem (this is a code smell, obviously, but sometimes it has to be done). Being able to do this at-will in a large code-base, however, can easily lead to problems. Maybe you do your patch in an obscure area of the code, and a new developer writes a method that uses the _expected_ behaviour, and then spends a day or so trying to work out why her code isn’t working correctly. Or even worse, somebody decides to write another monkey-patch on the same method, meaning that the behaviour in production becomes a Fun Race Condition That Wakes You Up At 3am depending on which one is executed by Ruby first. The real answer is Don’t Monkey-Patch, and enforce it with hammers. But sometimes it’s not possible to avoid that monkey. Thankfully, Ruby 2 delivers a way of at least limiting the scope of patches with _Refinements_.

module WithAMonocle
  refine String do
    def all_is_love
      '❤' * self.length
    end
  end
end

Module#refine takes a class and a block, and in that block, you can extend or redefine to your heart’s content. But:


pry(main)> 'hello’.all_is_love                                             
NoMethodError: undefined method `all_is_love’ 
for "hello":String

Refinements need to be specifically activated with the using keyword:


using WithAMonocle
puts ‘hello’.all_is_love
=> "❤❤❤❤❤”

Okay, so far, much the same as normal monkey-patching. But the magic is that the patch only exists in that scope. So if we embed them in a Class, the refinement only exists within that class. For example:


class SplendidMonkey
  using WithAMonocle
  puts 'fff’.all_is_love
end

❤❤❤
=> nil

class UncouthMonkey
  puts 'fff’.all_is_love
end

=> NoMethodError: undefined method `all_is_love’ 
for "fff":String

The monkey-patch is restricted to the class where it is activated. As you can imagine, this is all sorts of useful - not only do you not pollute the global scope with your redefinitions or extensions, but you also make it explicit that you’re using that particular patch (and also providing a giant big clue where the patch is defined).

So, still, don’t go crazy with your monkey-patching, but, if you have to use them (and you’re running on a current Ruby 2.1.x version), consider using refinements as a way to limit their problematic features…

(however, do be aware that there are a few odd things that may bite you if you’re not paying attention. In particular:


module WithAShortMonocle
  refine String do
    HEART_LENGTH = 1
    def all_is_love
      '❤' * HEART_LENGTH
    end
  end
end

Constants and class variables within a refinement belong to the module, not the class being refined:


pry(main)> String::HEART_LENGTH
NameError: uninitialized constant 
String::HEART_LENGTH
pry(main)> WithAShortMonocle::HEART_LENGTH
=> 1

So be aware of that when adding your refinements)