Как мне автоматически привести экземпляр класса к Float в Ruby? - PullRequest
3 голосов
/ 10 декабря 2011

У меня есть класс Angle, который я хочу вести себя как поплавок с дополнительным поведением. Я создал класс, который будет содержать float и прокси для всех неизвестных ему методов:

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

Работает нормально. Однако, когда я пытаюсь использовать его с операциями триггера, например, Math.cos, я получаю:

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

Я знаю, что могу использовать Float (a) для преобразования в float, но это неудобно, так как я хочу, чтобы этот класс вел себя как float. Есть ли способ автоматически конвертировать Angle в float в этих случаях?

Ответы [ 2 ]

3 голосов
/ 10 декабря 2011

Глядя на реализацию Math.cos , вы можете увидеть, что он вызывает макрос Need_Float, , который затем вызывает функцию rb_to_float . Строка 2441 из rb_to_float проверяет, имеет ли переданный объект тип Numeric . Таким образом, кажется, что единственный способ заставить ваш собственный класс действовать как поплавок в семействе функций Math - это наследовать его от Numeric или потомка Numeric. Таким образом, эта модификация вашего кода работает как положено:

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

Я не уверен, какие побочные эффекты будут иметь наследование от Numeric, но, к сожалению, это выглядит как единственный способ заставить ваш код работать так, как вы этого хотите.

0 голосов
/ 11 декабря 2011

Это то, что я придумал самостоятельно. Math - единственный модуль, который меня действительно интересует, поэтому я могу создать для него прокси:

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

Теперь с определением класса Angle сверху:

a = Angle.new(0.0)
# => #<Angle:0x00000000e6dc28 @angle=0.0> 
Math.cos(a)
# => 1.0 
...