3

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?

mskfisher
  • 3,291
  • 4
  • 35
  • 48
Brian Wong
  • 140
  • 2
  • 7

2 Answers2

3

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.

phiggy
  • 898
  • 8
  • 9
0

This is what I came up with on my own. Math is the only module that I'm really interested in, so I can make a proxy for it:

module Stdlib; end
::Stdlib::Math = ::Math
module AngleMath
  # Copy constants
  Stdlib::Math.constants.each do |c|
    self.const_set(c, ::Stdlib::Math.const_get(c))
  end

  def self.map_angles_to_floats(args)
    args.map do |a|
      a.kind_of?(Angle)? a.to_f: a
    end
  end

  def self.method_missing(message, *args, &block)
    if block_given?
      ::Stdlib::Math.public_send(message, *map_angles_to_floats(args), &block)
    else
      ::Stdlib::Math.public_send(message, *map_angles_to_floats(args))
    end
  end
end
::Math = AngleMath

Now with the Angle class definition from above:

a = Angle.new(0.0)
# => #<Angle:0x00000000e6dc28 @angle=0.0> 
Math.cos(a)
# => 1.0 
Brian Wong
  • 140
  • 2
  • 7