Recommended Approach to Monkey Patching a Class in Ruby

Recommended approach to monkey patching a class in ruby

Honestly, I used to use the 1st form (reopening the class), as it feels more natural, but your question forced me to do some research on the subject and here's the result.

The problem with reopening the class is that it'll silently define a new class if the original one, that you intended to reopen, for some reason wasn't defined at the moment. The result might be different:

  1. If you don't override any methods but only add the new ones and the original implementation is defined (e.g., file, where the class is originally defined is loaded) later everything will be ok.

  2. If you redefine some methods and the original is loaded later your methods will be overridden back with their original versions.

  3. The most interesting case is when you use standard autoloading or some fancy reloading mechanism (like the one used in Rails) to load/reload classes. Some of these solutions rely on const_missing that is called when you reference undefined constant. In that case autoloading mechanism tries to find undefined class' definition and load it. But if you're defining class on your own (while you intended to reopen already defined one) it won't be 'missing' any longer and the original might be never loaded at all as the autoloading mechanism won't be triggered.

On the other hand, if you use class_eval you'll be instantly notified if the class is not defined at the moment. In addition, as you're referencing the class when you call its class_eval method, any autoloading mechanism will have a chance to locate class' definition and load it.

Having that in mind class_eval seems to be a better approach. Though, I'd be happy to hear some other opinion.

Is a bad practice to monkey patch a base ruby class?

Monkey patching isn't considered to be a bad practice unless you are writing odd methods that do not have PatchedClass-related behavior (for example, String.monkeyPatchForMakingJpegFromString is rather bad, but Jpeg.fromString is good enough.)

But if your project is rather large, the libraries that you use in it may happen to have colliding patches, so you may have one more problem with all these patching stuffs. In Ruby 2.0, refinements come to an aid. They work as follows: you define a module, refine your (even core) class in it, and then use that module where it's necessary. So, in your code it works as:

YourClass.new.refinedMethodFromCoreClass #=> some result

But

CoreClass.refinedMethodFromCoreClass

produces undefined method exception.

That's all monkey patching stuff: monkey patching is useful and convenient, but refinements add some features, that make your code more secure, maintainable and neat.

Un-monkey patching a class/method in Ruby

Expanding on @Tilo's answer, use alias again to undo the monkey patching.

Example:

# Original definition
class Foo
def one()
1
end
end

foo = Foo.new
foo.one

# Monkey patch to 2
class Foo
alias old_one one
def one()
2
end
end

foo.one

# Revert monkey patch
class Foo
alias one old_one
end

foo.one

monkey patching vs class_eval?

With class_eval you can do more dynamic things:

>> met = "hello" #=> "hello"
>> String.class_eval "def #{met} ; 'hello' ; end" #=> nil
>> "foo".hello #=> "hello"

Organizing monkey patches

It doesn't matter where you put the include call.

Calling String.include will always monkey patch the one String class that is used by all the strings in the entire object space. So best put the instruction at the top level as to not mislead readers of the code.

Monkey patching is always global.

It is a powerful feature and can be used for good.

If you are authoring a gem be aware that you're sharing a global namespace with others. The same is also true for top-level modules and even the gem name though. Shared namespaces are just a reality of shared code.

If you are looking for lexically scoped monkey patches look into the new refinement feature that was introduce with Ruby 2.

Refinements are an idea taken from Smalltalk's class boxes. Refinements are not without their own issues though, for example they lack support for introspection and reflection. Thus essentially making them stealth and unfit for production use.

If you are looking to limit the monkey patches to some string object only, consider either subclassing String or calling extend on an instance.

How to monkey patch class included module?

I was able to solve this by first aliasing the the original method, then by calling the aliased original in my overridden method.

module_eval do
alias_method :get_company_old, :get_company
define_method :get_company do |*args|
# My new code here
send :get_company_old, *args
end

The problem is that super in this context does not work. This solved the issue for me.

monkey patching the * operator

other.x[i]*=self

is the same as

other.x[i] = other.x[i] * self

or to make it really explicit

other.x().[]=(i)(other.x().[](i).*(self))

other.x[i] is an Integer, and self is also an Integer (in this case 3).
In your Integer method, you call other.x, but when you multiply some_integer * 3, then in your Integer#* method, other is 3, so you are calling 3.x which indeed doesn't exist.

Note that your Vektor#* method is now also broken, because it, too, needs to multiply two Integers, but you just overwrote the Integer#* method which knows how to multiply two Integers (and actually also knows how to multiply an Integer with any object that correctly implements the Numeric#coerce protocol) with one that only knows how to multiply an Integer and a Vektor, and thus no longer knows how to multiply two Integers.

Another problem is that in your Vektor#* method, you check for other being an instance of Numeric, but then the other way around you only implement multiplication for Integers. This makes your multiplication asymmetric, e.g. some_vektor * 1.0 will work, but 1.0 * some_vektor won't.

The correct implementation would be to not touch Integer at all, and simply implement the numeric coercion protocol. That will solve all of your problems: you don't have to monkey-patch anything, and your Vektors will automatically work with any Numeric in the core library, the standard library, the Ruby ecosystem and in fact even with ones that haven't even been written yet. Something like this:

class Vektor
def self.inherited(*)
raise TypeError, "#{self} is immutable and cannot be inherited from."
end

def initialize(*vektors)
self.x = if vektors.size.zero?
Array.new(3, 0)
else
vektors
end.freeze
end

singleton_class.alias_method :[], :new

alias_method :length, def size
x.size
end

def coerce(other)
[self, other]
end

def *(other)
case other
when Vektor
raise ArgumentError, "the vektors don't have the same length!" if size != other.size
x.zip(other.x).map {|a, b| a * b }.sum
when Numeric
self.class.new(*x.map(&other.method(:*)))
else
a, b = other.coerce(self)
a * b
end
end

protected

attr_reader :x # `x` should not be writeable by anybody!

private

attr_writer :x

freeze
end

You will notice that I made some changes to your code:

  • Vektors are now immutable and Vektor#* returns a new Vektor instead of mutating self. It is generally a good idea to keep your objects immutable as far as possible, but it is especially important (and expected, really) for "number-like" objects such as Vektors. You would be terribly surprised if 2 * 3 didn't return 6 but rather made 2 have the value 6, wouldn't you? But that is exactly what your code was doing with Vektors!
  • x is no longer exposed to everybody, and most importantly, no longer writeable by everybody.
  • I replaced all of your loops with higher-level iteration constructs. As a general rule, if you are writing a loop in Ruby, you are doing something wrong. There are so many powerful methods in Enumerable that you should never need a loop.

I also wrote some tests to demonstrate that without even touching any class outside of Vektor, we now support multiplication of int * vek, vek * int, float * vek, vek * float, rational * vek, vek * rational, complex * vek, and vek * complex, simply by implementing the Numeric#coerce coercion protocol.

require 'test/unit'
class VektorTest < Test::Unit::TestCase
def test_that_creating_an_empty_vektor_actually_creates_a_zero_vektor_of_dimension_3
v = Vektor.new
assert_equal 3, v.size
end

def test_that_square_brackets_is_an_alias_for_new
v = Vektor[]
assert_equal 3, v.size
end

def test_that_we_can_multiply_two_trivial_vektors
v1 = Vektor[2]
v2 = Vektor[3]
assert_equal 6, v1 * v2
end

def test_that_we_can_multiply_two_nontrivial_vektors
v1 = Vektor[2, 3, 4]
v2 = Vektor[5, 6, 7]
assert_equal 56, v1 * v2
end

def test_that_we_can_multiply_a_trivial_vektor_with_an_integer
v = Vektor[2]
assert_equal Vektor[6], v * 3 # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_trivial_vektor_with_an_integer_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { v * 3 }
end

def test_that_we_can_multiply_a_nontrivial_vektor_with_an_integer
v = Vektor[2, 3, 4]
assert_equal Vektor[6, 9, 12], v * 3 # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_nontrivial_vektor_with_an_integer_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { v * 3 }
end

def test_that_we_can_multiply_an_integer_with_a_trivial_vektor
v = Vektor[2]
assert_equal Vektor[6], 3 * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_an_integer_with_a_trivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { 3 * v }
end

def test_that_we_can_multiply_an_integer_with_a_nontrivial_vektor
v = Vektor[2, 3, 4]
assert_equal Vektor[6, 9, 12], 3 * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_an_integer_with_a_nontrivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { 3 * v }
end

def test_that_we_can_multiply_a_trivial_vektor_with_a_float
v = Vektor[2]
assert_equal Vektor[6.0], v * 3.0 # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_trivial_vektor_with_a_float_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { v * 3.0 }
end

def test_that_we_can_multiply_a_nontrivial_vektor_with_a_float
v = Vektor[2, 3, 4]
assert_equal Vektor[6.0, 9.0, 12.0], v * 3.0 # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_nontrivial_vektor_with_an_float_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { v * 3.0 }
end

def test_that_we_can_multiply_a_float_with_a_trivial_vektor
v = Vektor[2]
assert_equal Vektor[6.0], 3.0 * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_float_with_a_trivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { 3.0 * v }
end

def test_that_we_can_multiply_a_float_with_a_nontrivial_vektor
v = Vektor[2, 3, 4]
assert_equal Vektor[6.0, 9.0, 12.0], 3.0 * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_float_with_a_nontrivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { 3.0 * v }
end

def test_that_we_can_multiply_a_trivial_vektor_with_a_rational
v = Vektor[2]
assert_equal Vektor[6r], v * 3r # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_trivial_vektor_with_a_rational_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { v * 3r }
end

def test_that_we_can_multiply_a_nontrivial_vektor_with_a_rational
v = Vektor[2, 3, 4]
assert_equal Vektor[6r, 9r, 12r], v * 3r # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_nontrivial_vektor_with_an_rational_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { v * 3r }
end

def test_that_we_can_multiply_a_rational_with_a_trivial_vektor
v = Vektor[2]
assert_equal Vektor[6r], 3r * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_rational_with_a_trivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { 3r * v }
end

def test_that_we_can_multiply_a_rational_with_a_nontrivial_vektor
v = Vektor[2, 3, 4]
assert_equal Vektor[6r, 9r, 12r], 3r * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_rational_with_a_nontrivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { 3r * v }
end

def test_that_we_can_multiply_a_trivial_vektor_with_a_complex_number
v = Vektor[2]
assert_equal Vektor[6i], v * 3i # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_trivial_vektor_with_a_complex_number_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { v * 3i }
end

def test_that_we_can_multiply_a_nontrivial_vektor_with_a_complex_number
v = Vektor[2, 3, 4]
assert_equal Vektor[6i, 9i, 12i], v * 3i # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_nontrivial_vektor_with_an_complex_number_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { v * 3i }
end

def test_that_we_can_multiply_a_complex_number_with_a_trivial_vektor
v = Vektor[2]
assert_equal Vektor[6i], 3i * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_complex_number_with_a_trivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2]
assert_nothing_raised { 3i * v }
end

def test_that_we_can_multiply_a_complex_number_with_a_nontrivial_vektor
v = Vektor[2, 3, 4]
assert_equal Vektor[6i, 9i, 12i], 3i * v # this will fail because you haven't implemented equality!
end

def test_that_multiplying_a_complex_number_with_a_nontrivial_vektor_at_least_does_not_raise_an_exception
v = Vektor[2, 3, 4]
assert_nothing_raised { 3i * v }
end
end

Let's also add some methods that you generally always need to implement to make your objects work with the rest of the Ruby ecosystem:

class Vektor
def ==(other)
x == other.x
end

def eql?(other)
other.is_a?(Vektor) && self == other
end

def hash
x.hash
end

def to_s
"(#{x.join(', ')})"
end

def inspect
"Vektor#{x.inspect}"
end
end

If you absolutely must use monkey-patching, then it is important that you retain access to the old version of the method that you are monkey-patching. The tool to do this is the Module#prepend method. It would look something like this:

class Vektor
def self.inherited(*)
raise TypeError, "#{self} is immutable and cannot be inherited from."
end

attr_reader :x # `x` should not be writeable by anybody!

def initialize(*vektors)
self.x = if vektors.size.zero?
Array.new(3, 0)
else
vektors
end.freeze
end

singleton_class.alias_method :[], :new

alias_method :length, def size
x.size
end

def *(other)
case other
when Vektor
raise ArgumentError, "the vektors don't have the same length!" if size != other.size
x.zip(other.x).map {|a, b| a * b }.sum
when Numeric
self.class.new(*x.map(&other.method(:*)))
end
end

private

attr_writer :x

freeze
end

(Mostly identical, but without the coerce method and without the else clause in the case expression, and the x reader needs to be public.)

module IntegerTimesVektorExtension
def *(other)
return Vektor[*other.x.map(&method(:*))] if other.is_a?(Vektor)
super
end
end

And because monkey-patching core classes is really dangerous, we use a refinement, to make sure that the monkey-patch is only active where you explicitly activate the refinement with using IntegerTimesVektorRefinement.

module IntegerTimesVektorRefinement
refine Integer do
prepend IntegerTimesVektorExtension
end
end

Now, we can do something like this:

v = Vektor[2, 3, 4]

5 * v
# `*': Vektor can't be coerced into Integer (TypeError)

using IntegerTimesVektorRefinement

5 * v
#=> <#<Vektor:0x00007fcc88868588 @x=[10, 15, 20]>

In the dark old times, before Module#prepend existed, we had to resort to other dirty tricks to be able to keep the monkey-patched method around. However, as of the release of Ruby 2.0 on February, 24th 2013, which includes Module#prepend, these tricks are no longer necessary, and should not be used, nor taught. This includes the alias_method chain trick, which looks like this:

class Integer
alias_method :original_mul, :*

def *(other)
return Vektor[*other.x.map(&method(:*))] if other.is_a?(Vektor)
original_mul(other)
end
end

But, as mentioned: you should not do this.

The best solution is to implement the coerce protocol. Really not "just" the best solution but the only correct solution.

If, for some reason, you do not want to implement the coerce protocol, then the best solution is Module#prepend. Ideally with refinement, but please be aware that not all Ruby implementations implement refinements.

If you really, really, really, must do monkey-patching, and are using a ten-year old, unsupported, unmaintained, obsolete, outdated version of Ruby and thus cannot use Module#prepend, then there are still better solutions out there than alias_method, such as grabbing the instance method as a Method object and storing it in a local variable that you close over.

Using alias_method here is not necessary, if not outright wrong, and bad, outdated, obsolete practice.

How to monkey patch a class using a module?

You're looking for a feature like refinements:

stylable.rb:

module Stylable
refine Array do
def styled
"\n" << "*"*72 << self.to_s << "*"*72 << "\n"
end
end
end

printer.rb:

require './stylable'
class Printer
using Stylable
# ...
end
Printer.new([1,2,3]).print

The advantage of using refinements is that this monkey-patch works only in scope of Printer class, so it's less likely to break something.

The reason your original approach doesn't work is that instead of monkey-patching ::Array class, you implemented a new Stylable::Array class (note the namespace).

If you're running Ruby < 2.0 and you don't want to monkey-patch Array globally, you could create custom class that inherits from array:

class StylableArray < Array
def styled
"\n" << "*"*72 << self.to_s << "*"*72 << "\n"
end
end

and use it in your Printer file:

Printer.new(StylableArray.new([1,2,3]))

The other way is monkey-patching Array globaly, which isn't recommended.



Related Topics



Leave a reply



Submit