0
votes

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)

Edited:

the assignment text:

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. :)

1
In the when Numeric block, you call self.x[i]*=other. If my understanding is correct, self.x[i] is a number so this ends up calling <number>.mul(<number>). So I think you need to do a typecheck in the mul method to see whether other is a Numeric or Vektor. Although I haven't fully followed your logic heremax pleaner

1 Answers

0
votes
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.