views:

209

answers:

2

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)

A: 

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
James A. Rosen
mmm sexy :) btw, i needed to change the first line to "Fixnum.class_eval do" or (the equivalent?) "class Fixnum"
Evan Griffiths
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 ?
banister
ok, this code is crazy :) not only do you not need a class_eval (an ordinary class body will do fine) you are doing ridiculous things with unbound method objects that are completely unnecessary. Why not just use alias_method instead? ;)Not to mention that monkey-patching a core class is a huge no-no too :)
banister
`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).
Jörg W Mittag
I am sure it's a good idiom to use in _some_ circumstances, but not in this case. And your argument regarding alias_method 'polluting' the namespace' is weak at best in _this_ context where an even greater evil (monkey-patching a core class) is being committed.If you have a link to a blog explaining the usefulness of the binding-unbound-method idiom is useful in THIS context (where afaik most people just use alias method chaining) then post it here :)
banister
I'm not really *for* monkey-patching core classes, but this sort of commutivity problem is well-established in object-oriented programming. And Jörg Mittag is exactly right about why I suggested the unbound method instead of `alias_method` -- there wouldn't be any reason to call `original_times`, so I prevented people from doing so. It would just complicate the API.
James A. Rosen
banister's suggestion for `coerce` is a much better solution for the commutivity problem, though. +1 for it.
James A. Rosen
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.
James A. Rosen
+8  A: 

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

banister
+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.
zenazn
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 ;)
Evan Griffiths