Search code examples
rubycastingfloating-pointproxy-classes

How do I automatically cast a class instance to Float in Ruby?


I have an Angle class that I want to behave like a float, with additional behavior. I have created the class to contain a float and proxy all unknown methods to it:

class Angle
  include Math
  def initialize(angle=0.0)
    @angle = Float(angle)

    # Normalize the angle
    @angle = @angle.modulo(PI*2)
  end

  def to_f
    @angle.to_f
  end

  # Other functionality...

  def method_missing(message, *args, &block)
    if block_given?
      @angle.public_send(message, *args, &block)
    else
      @angle.public_send(message, *args)
    end
  end
end

It works fine. However when I try to use it with trig operations, e.g. Math.cos, I get:

> a = Angle.new(0.0)
 => #<Angle:0x00000000cdb220 @angle=0.0> 
@angle=0.0
> Math.cos(a)
TypeError: can't convert Angle into Float

I know I can use Float(a) to convert to a float, but it's inconvenient since I want this class to behave like a float. Is there a way to automatically convert Angle to float in these cases?


Solution

  • Looking at the implementation of Math.cos, you can see it calls a macro called Need_Float, which then calls a function rb_to_float. Line 2441 of rb_to_float checks to see if the object passed in is of type Numeric. So it seems the only way to have your own class act as a float in the Math family of functions is to have it inherit from Numeric or a descendant of Numeric. Thus, this modification of your code works as expected:

    class Angle < Numeric
      include Math
      def initialize(angle=0.0)
        @angle = Float(angle)
    
        # Normalize the angle
        @angle = @angle.modulo(PI*2)
      end
    
      def to_f
        @angle.to_f
      end
    
      # Other functionality...
    
      def method_missing(message, *args, &block)
        if block_given?
          @angle.public_send(message, *args, &block)
        else
          @angle.public_send(message, *args)
        end
      end
    end
    
    if __FILE__ == $0
      a = Angle.new(0.0)
      p Math.cos(a)
    end
    

    I'm not sure what side effects inheriting from Numeric will have, but unfortunately this looks like the only way to have your code work the way you want it to.