14

i've been messing around with ruby and opengl for entertainment purposes, and i decided to write some 3d vector/plane/etc classes to pretty up some of the math.

simplified example:

class Vec3
    attr_accessor :x,:y,:z

    def *(a)
        if a.is_a?(Numeric) #multiply by scalar
            return Vec3.new(@x*a, @y*a, @z*a)
        elsif a.is_a?(Vec3) #dot product
            return @x*a.x + @y*a.y + @z*a.z
        end
    end
end

v1 = Vec3.new(1,1,1)
v2 = v1*5 #produces [5,5,5]

which all fine and dandy, but i also want to be able to write

v2 = 5*v1

which requires adding functionality to Fixnum or Float or whatever, but i couldn't find a way to overload or extend fixnum's multiplication without replacing it entirely. is this possible in ruby? any tips?

(obviously i can just write all my multiplications in the correct order if i need to)

2
  • Just for the record, change @x*s, @y*s, @z*s to @x*a, @y*a, @z*a, otherwise your code is broken. Commented Dec 6, 2009 at 7:39
  • thanks, copied code from 2 places at once >< should be fixed now Commented Dec 6, 2009 at 12:01

2 Answers 2

24

Using coerce is a MUCH better approach than monkey-patching a core class:

class Vec3
    attr_accessor :x,:y,:z

    def *(a)
        if a.is_a?(Numeric) #multiply by scalar
            return Vec3.new(@x*a, @y*a, @z*a)
        elsif a.is_a?(Vec3) #dot product
            return @x*a.x + @y*a.y + @z*a.z
        end
    end

    def coerce(other)
        return self, other
    end
end

if you define v as v = Vec3.new then the following will work: v * 5 and 5 * v The first element returned by coerce (self) becomes the new receiver for the operation, and the second element (other) becomes the parameter, so 5 * v is exactly equivalent to v * 5

Sign up to request clarification or add additional context in comments.

3 Comments

+1 for coerce. On behalf of the person who has to debug your code, please don't monkeypatch core classes unless super-duper-absolutely necessary.
this worked great for what i needed. if i run into a similar example that can't be commutative then i suppose i'll monkey patch as needed ;)
Coerce should always return self as the second argument! Otherwise you mess up the commutativity of the arguments. Instead, coerce should cast the argument to a type that can be multiplied (for example by: Vec3.new(other, other, other)). Obviously this is not without it's own problems.
0

I believe the following will do what you want, though banister's suggestion to use coerce instead of monkey-patching Numeric is a preferred method. Use this method only if necessary (for example if you only want some binary operands to be transitive).

Fixnum.class_eval do
  original_times = instance_method(:*)
  define_method(:*) do |other|
    if other.kind_of?(Vec3)
      return other * self
    else
      return original_times.bind(self).call(other)
    end
  end
end

8 Comments

mmm sexy :) btw, i needed to change the first line to "Fixnum.class_eval do" or (the equivalent?) "class Fixnum"
is it not possible just to define it directly on Fixnum class without the class_eval, and to do a regular def rather than a define_method ?
alias_method unnecessarily pollutes the namespace. This idiom is neither "ridiculous" (it is a well-known idiom, the inner workings of which as well as the necessity have been well-documented in many a blog post) nor is it "completely unnecessary" (it is, in fact, the only idiom known to work, AFAIK).
banister's suggestion for coerce is a much better solution for the commutivity problem, though. +1 for it.
Oh, and banister: an "ordinary class body" will work fine for Numeric, which is surely defined at this point, but is dangerous in other cases, as it might prevent an autoload statement from firing.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.