domingo, 1 de abril de 2012

Menos precision por favor

Estoy cursando numerico en fiuba y se dio la necesidad de probar varios algoritmos en distintos grados de precision (el ejemplo mas claro de esto, es el tipo de datos double y float de C)
, como todo rubyfan decidi hacerlo en ruby, pero ruby solo tiene un tipo de dato de punto flotante, llamado Float, que wrappea el double nativo de C

El truco, como en otros lenguajes con solo una precision de punto flotante, es emularlo; es decir, truncarlo a la precision deseada despues de cada operacion. Ruby tiene soluciones interesantes para hacer este tipo de cosas.

El decorator


En un principio, usamos un patron llamado decorator, con la idea de modificar el comportamiento del float:

Nuestro FloatDecorator repite cada operacion en el objeto "decorado", pero al retornar el resultado lo trunca y lo decora tambien asi mantiene el comportamiento

Asi, la raiz cuadrada de dos 2**0.5 da 1.41 y no 1.4142135623730951






La manera mas rubyway de implementar un decorator es usando method_missing En este caso se implementaria asi:
class FloatPrecisionDecorator
  def initialize(inner, factor)
    @factor = factor
    @inner = inner.to_f
  end

  def method_missing(m,*x)
    # todas las operaciones sobre el numero se ejecutan sobre el float verdadero
    # y se obtiene el verdadero resultado con precision completa del Float original
    verdadero_resultado = @inner.send(m,*x)
    # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir
    reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor
    FloatPrecisionDecorator.new(reduced, @factor)
  end
end

# esto da un FloatPrecisionDecorator con un 1.41, adentro, no un 1.4142135623730951
p FloatPrecisionDecorator.new(2,100)**0.5

Inspect


Pero cuando se hace el print por salida estandar, aparece algo asi:

#..floatprecisiondecorator:0x8cd63f8 factor="10," inner="1.4"...

¿ No seria mejor que simplemente mostrara el 1.41 ? Para eso hay que sobrecargar el metodo inspect

class FloatPrecisionDecorator
  def inspect
    # para que llame al inspect del float decorado
    @inner.inspect
  end
end

# ahora si va a mostra simplemente 1.41
p FloatPrecisionDecorator.new(2,100)**0.5

Numeric


Es mejor si se puede hacer asi:


4.0.to_reduced_precision(:decimals => 2)

4.0.to_rp(:decimals => 2)

Para eso lo mejor es agregarle el metodo a la clase numeric:

class Numeric
  def self.reduce_precision(number, factor)
    (number.to_f * factor).round.to_f / factor
  end

  def to_single_precision(options)
    unless options[:factor]
      options[:factor] = (options[:base]||10) ** (options[:decimals]||10)
    end

    factor = options[:factor]

    FloatPrecisionDecorator.new(Numeric.reduce_precision(to_f, factor), factor)
  end
end

Coerce



Puede surgir que se tenga que sumar un numero de precision reducida a un float, pero el metodo + de la clase Float de ruby no tiene forma de saber como sumar el numero que implementamos nosotros. Esos casos ruby lo contempla con el metodo coerce, que habilita conmutar los numeros en una operacion matematica, asi:
class FloatPrecisionDecorator
  def coerce(other)
    return self, other
  end
end

inifinite?, nan? y demas


Si se hace a nuestro numero


p 2.to_sp(:decimals => 10).infinite?


Va a devolver 0.0, cuando se supone que el metodo inifnite debe devolver true o false, esto ocurre porque inifnite? tambien llama a method_missing y este trata de truncar y wrappear lo que sea, lo mejor en estos casos es evitar modificar objetos que no sean numeros. Habra que modificar method_missing:

class FloatPrecisionDecorator
  def method_missing(m,*x)
    # todas las operaciones sobre el numero se ejecutan sobre el float verdadero
    # y se obtiene el verdadero resultado con precision completa del Float original
    verdadero_resultado = @inner.send(m,*x)

    # si es numeric, truncar y wrappear
    if Numeric === verdadero_resultado
      # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir
      reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor
      FloatPrecisionDecorator.new(reduced, @factor)
    else
      # si no, devolve el resultado como es
      verdadero_resultado
    end
  end
end

Codigo completo





class Numeric
  def self.reduce_precision(number, factor)
    (number.to_f * factor).round.to_f / factor
  end

  def to_single_precision(options)
    unless options[:factor]
      options[:factor] = (options[:base]||10) ** (options[:decimals]||10)
    end

    factor = options[:factor]

    FloatPrecisionDecorator.new(Numeric.reduce_precision(to_f, factor), factor)
  end
end

class FloatPrecisionDecorator
  def initialize(inner, factor)
    @factor = factor
    @inner = inner.to_f
  end

  def method_missing(m,*x)
    # todas las operaciones sobre el numero se ejecutan sobre el float verdadero
    # y se obtiene el verdadero resultado con precision completa del Float original
    verdadero_resultado = @inner.send(m,*x)

    # si es numeric, truncar y wrappear
    if Numeric === verdadero_resultado
      # se reduce la precision, multiplicando por el factor, redondeando y volviendo a dividir
      reduced = (verdadero_resultado.to_f * @factor).round.to_f / @factor
      FloatPrecisionDecorator.new(reduced, @factor)
    else
      # si no, devolve el resultado como es
      verdadero_resultado
    end
  end

  def coerce(other)
    return self, other
  end

  def inspect
    # para que llame al inspect del float decorado
    @inner.inspect
  end
end

p FloatPrecisionDecorator.new(2,10).infinite?