Ruby: Reescritura y yield

Durante unas pruebas en el trabajo, enseñando a mi mujer (Marga), cómo funciona Ruby con su mayor potencia, la reescritura de código y los yield, pensé: con lo que me ha costado… mejor lo guardo en el blog. Y esto es :-)

Orientación a Objetos, no a Clases

Teniendo en cuenta de que Ruby es un lenguaje orientado a objetos (y no a clases), huelga decir que, no solo se puede redefinir una clase (es decir, el objeto plantilla del que se generan otros objetos), sino que también se puede redefinir un objeto instanciado sin que afecte al resto de objetos del mismo tipo.

Sobre la orientación a objetos de Ruby, vamos a hacer este simple ejercicio desde irb para que se pueda ver algo más claro:

class A
end
 
a = A.new
 
a.class   # A
A.class  # Class
Class.class # Class

Como se puede ver, el objeto a es de tipo A, el objeto (que no clase) A es de tipo Class, y el objeto (que tampoco clase) Class es recursivo en sí mismo, ya que Class es el tipo raíz del que parten todos los objetos… como en Java lo es Object.

Reescribiendo Clases

Una de las mayores potencias de Ruby, es que una clase definida, se puede redefinir, ya sea para cambiar un comportamiento específico, o para agregarle mayor funcionalidad. Esto de cara a la programación orientada a objetos, es una aberración, realmente, porque rompe la encapsulación… peeeero, de cara al pragmatismo, es muy útil poder modificar un comportamiento general para adaptarlo a uno específico sin necesidad de tener que reescribir de qué clase se hereda.

Haciendo una prueba:

class A
end
 
a = A.new
puts a    # #<A:0x000000000000>
 
class A
  def to_s
    "Clase A"
  end
end
 
puts a    # Clase A
 
b = A.new
 
class << b
  def to_s
    "objeto b"
  end
end
 
puts b    # objeto b
puts a    # Clase A

Con esto se puede comprobar, que la redefinición de métodos se puede conseguir, tanto para objetos como para clases.

Y ahora… el yield

Otra de las potencias de Ruby es que, podemos definir un código, dejando una definición a medias para poder ampliarla en el momento el que sea llamada, por ejemplo:

def ordena( vector )
     (..(vector.size-2)).each do |i|
         k = i
         (i+1..(vector.size-1)).each do |j|
             k=j if yield(vector[k], vector[j])
         end
         if k!=i
             tmp = vector[k]
             vector[k] = vector[i]
             vector[i] = tmp
         end
     end
end
 
vector = [ 5, 3, 2, 4, 6, 1 ]
 
ordena(vector) do |e1, e2|
    (e1 > e2)
end
 
puts vector  # [1, 2, 3, 4, 5, 6]
 
ordena(vector) do |e1, e2|
    (e1 < e2)
end
 
puts vector  # [6, 5, 4, 3, 2, 1]

En este ejemplo de código, la función yield no está definida, sino que equivale al bloque de código que se especifica más tarde, cuando se llama a la función ordena, en el caso mostrado, es como si las funciones yield fuesen en el primer caso:

def yield(e1, e2)
    (e1 > e2)
end

Y en el segundo caso, exactamente igual pero cambiando la comparación para que la ordenación sea en sentido descendente.

La definición del código es in-situ, en el momento de usarse es cuando se especifica qué hace yield.

Se puede hacer algo más complejo, por ejemplo, cambiando el array de enteros por un array de objetos específicos que tengan una comparación algo más complicada. Aún así, el código de ordena no habría que cambiarlo. Solo la llamada, y su bloque adjunto.

Conclusiones

Para los que no conozcan bien Ruby, esto les habrá sonado a chino, para los que lo conozcan bien, quizás algo ya más que sabido, pero lo importante, es que se tenga claro el concepto y lo que se puede hacer, ya que, la necesidad, de seguro, surgirá después.