im new to ruby and trying to understand monkey patching.
so while practicing on an old project which is a simple Vektor calculator.
I wrote the following code which will allow me to multiply a Vektor with a number and two vectors with each other:
class Vektor
attr_accessor :x #instead of getter and setter!-
def initialize (*vektors)
if vektors.length==0
@x=Array.new(3,0)
else
@x=vektors
end
end
def size
@x.length
end
def *(other)
case other
when Vektor
if (self.size != other.size)
return "the vektors don't have the same length!"
else
result=0
for i in 0 ... other.size
result += (self.x[i] * other.x[i])
end
return result
end
when Numeric
for i in 0 ... self.size
self.x[i]*=other
end
return self
end
end
end
and here is the integer class so i would be able to do the multiplication in an other way per example like this -> 5*vektor
class Integer
def mul(other)
for i in 0 ... other.size
other.x[i]*=self # this is the cause of the error!
end
return other
end
alias_method :* , :mul
end
obj1=Vektor.new(2,2,2)
puts 3*obj1
my problem is that I'm getting a raised error and I don't know why but I could assume it is because I'm using the *
in the alias_method
while it is being been used inside the mul
method.
the output is the following :
undefined method `x' for 3:Integer (NoMethodError)
Extend the Integer class with a monkey patch so that you can write 5 * vector instead of vector * 5.
Test whether this patch works via a suitable unit test.
Tip: You will want to use the alias keyword or the alias_method method to achieve the goal.
I totally understand the cause of the problem but I can't seem to find a workaround where I could prevent the error from occurring!.
this is what I'm currently at, which is working but in a Wrong way :(
class Integer
def mul(other)
case (other)
when Numeric
return self*other
when Vektor
for i in 0 ... other.size
other.x[i]*=self # this is the cause of the error!
end
return other
end
end
alias_method :**,:mul
end
obj1=Vektor.new(2,2,2)
puts obj1*3
puts 3**Vektor.new(3,3,3)
please note that all that I care about at this point is to know to extend my knowledge and not to get one silly note. :)
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 Integer
s, but you just overwrote the Integer#*
method which knows how to multiply two Integer
s (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 Integer
s.
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 Integer
s. 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 Vektor
s 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:
Vektor
s 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 Vektor
s. 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 Vektor
s!x
is no longer exposed to everybody, and most importantly, no longer writeable by everybody.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.