Métodos de aproximación en Ruby
Entre las tareas de una práctica para la asignatura de Mecánica Celeste (todo el código disponible en GitHub), he tenido que programar un par de métodos numéricos de aproximación iterativos. Resultó ser una aplicación muy práctica de los enumerables de Ruby y me parece un buen ejemplo del ahorro de código que puede suponer su uso.
Para abstraer distintos métodos iterativos de aproximación, definí una
clase abstracta Approximator de la que derivarían el resto. Permite
almacenar la aproximación actual y la tolerancia o error que se quiera
aceptar. Las clases derivadas implementan el método next_one que
calcula la siguiente aproximación a partir de la actual.
# Clase base para métodos numéricos de aproximación de funciones
class Approximator
  # Datos miembro
  #  - current:   valor de la aproximación actual
  #  - tolerance: valor de tolerancia
  attr_accessor :current, :tolerance
  
  def initialize initial, tolerance = DEFAULT_TOLERANCE
    self.current = initial
    self.tolerance = tolerance
  end
  # Método a implementar en las clases hijas
  def next_one
    raise NotImplementedError
  end
end
El punto clave para aprovechar la potencia de los enumeradores es
implementar un método each que haga uso de la función yield para
proporcionar los resultados parciales. En este caso, proporciona la
aproximación encontrada en cada iteración. Lo interesante de yield
es que no devuelve el control al método hasta que se necesita un nuevo
resultado, de forma que sólo se realizan los cálculos necesarios. Por
ejemplo, podría haber implementado (y lo hice, al principio) el método
each como un loop infinito, sin condición de parada, y podría
iterar las veces necesarias para obtener una precisión arbitraria
(suponiendo convergencia del método). Sin embargo, para poder hacer un
uso práctico de las clases, añadí la condición de parada con la
tolerancia.
Una vez implementado each, basta con incluir el módulo Enumerable
que añade toda la funcionalidad de los enumeradores, como to_a que
acumula en un array todos los elementos devueltos por each. Así,
para calcular la mejor aproximación basta con devolver el último
elemento de ese array.
class Approximator
  # Método que calcula y proporciona aproximaciones
  def each
    previous = Float::INFINITY
    until (current - previous).abs < tolerance
      # Proporciona una aproximación y espera a que se
      # pida la siguiente
      yield current
      previous = current
      self.current = next_one
    end
  end
  # Incluimos herramientas que permiten enumerar
  # las aproximaciones
  include Enumerable
  # Método que devuelve la última aproximación
  # para la tolerancia dada
  def approximate
    to_a.last
  end
end
Además de consultar la mejor aproximación, podremos realizar otras
tareas como obtener una lista de n iteraciones con take(n).
Por último, basta implementar especializaciones de esta clase con métodos de aproximación concretos. El siguiente es el método de Newton-Raphson, que permite calcular raíces de funciones a partir de sucesivas evaluaciones en la función y en su derivada.
class NewtonRaphson < Approximator
  # Datos miembro
  #  - function:   almacena la función f
  #  - derivative: almacena la derivada de f
  attr_accessor :function, :derivative
  
  def initialize initial, tolerance = DEFAULT_TOLERANCE, function, derivative
    super(initial, tolerance)
    self.function = function
    self.derivative = derivative
  end
  
  def next_one
    # Método de Newton-Raphson para encontrar raíces:
    # Calcula Φ(current) = current - f(current)/f'(current)
    current - function.call(current)/derivative.call(current)
  end
end
Un ejemplo de uso de esta implementación sería el cálculo de la raíz cuadrada de 5, como la raíz del polinomio correspondiente:
example = NewtonRaphson.new(
  3.0,
  ->(x) { x**2 - 5 },
  ->(x) { 2*x }
)
example.approximate
#=> 2.23606797749979
Otra especialización es la aproximación de la suma de una serie convergente, a partir de una función que evalúe el n-ésimo término:
class SeriesApproximator < Approximator
  # Datos miembro
  #  - term: función que evalúa el término n-ésimo de la serie
  #  - n: índice del término actual se la serie
  attr_accessor :term, :n
  
  def initialize tolerance = DEFAULT_TOLERANCE, term
    super(0, tolerance)
    self.term = term
    self.n = 0
  end
  # Calcula el siguiente término de la serie y lo suma a
  # la aproximación actual
  def next_one
    self.n += 1
    self.current += term.call(n)
  end
end
Podemos usarla para calcular la suma de Σ1/2^n:
series = SeriesApproximator.new(->(n) { 1.0/2**n })
series.approximate
#=> 0.999999999998181
En particular, las sumas de series nos pueden permitir aproximar los valores de algunas funciones en diversos puntos, utilizando por ejemplo desarrollos de Fourier.
Y si habéis leído hasta aquí, ¡gracias! Espero que os haya parecido útil y os sirva para aprovechar los enumerables en otros proyectos.