Refinements: Fancy Monkey-patching

Top hat, monocle and sweet stashA common gripe I’ve heard people bring up about Ruby is the fact that you can monkey-patch basically anything at any point during runtime. The result is that the world your code expects could potentially change under it and all hell breaks loose.  What if you included a 3rd party library that changed your expectations of .to_s?

Here’s a contrived example:

irb(main):001:0> 1.class
=> Integer
irb(main):002:0> 1.to_s
=> "1"
irb(main):003:0> class Integer
irb(main):004:1> def to_s
irb(main):005:2> "two"
irb(main):006:2> end
irb(main):007:1> end
=> :to_s
irb(main):008:two> 1.to_s
=> "two"
irb(main):009:two> 3.to_s
=> "two"
# WHAT HAVE I DONE TO MY LIFE?!

There is another way to monkey-patch that is safer — refinements!  Refinements have been in Ruby since 2.0 and essentially provides a way to namespace your monkey-patching efforts.

Creating and using refinements is pretty easy and requires:

  1. Create a module to hold your refinements
  2. USE that module inside your code

When making use of refinements the changes are scoped/limited within the scope of the module using said refinements. Here’s an example:

First we can create a module to hold our changes to the Integer class:

module IntegerRefinements
refine Integer do
def to_s
'TWO, ALWAYS TWO!!!!'
end
end
end

Next, via using, we can “use” this refinement in our own code!

module IntegerRefinements
refine Integer do
def to_s
'TWO, ALWAYS TWO!!!!'
end
end
end
class CrazyInteger
using IntegerRefinements
def self.crazy_string(number)
number.to_s
end
end

Now finally here’s an example of calling CrazyInteger and how using refinements has protected Integer#to_s outside of the scope of CrazyInteger‘s class.

# Our code calling `to_s` on an Integer has been refined
puts CrazyInteger.crazy_string(2)
# => "TWO, ALWAYS TWO!!!!"
puts CrazyInteger.crazy_string(3)
# => "TWO, ALWAYS TWO!!!!"
# Regular calls to Integer.to_s are still safe!
puts 1.to_s
# => "1"
puts 2.to_s
# => "2"