La duplicación en Ruby

Durante el día de hoy, hemos estado dando vueltas, tanto Daniel como yo, para ver si encontrábamos alguna forma de solucionar este problema que se nos había cruzado:

irb> a = [[1,2,3], [1,2,3]]
[[1,2,3], [1,2,3]]
irb> a = b
[[1,2,3], [1,2,3]]
irb> b[][] = 

irb> b
[[,2,3], [1,2,3]]
irb> a
[[,2,3], [1,2,3]]

Con este código se entiende que al igualar dos objetos, en Ruby, no se hace una copia del objeto, sino una referencia al mismo.

Si intentamos hacer algo como:

irb> b = a.clone
irb> c = a.dup

Nos encontramos, al realizar la prueba de modificación sobre b y c el mismo resultado. Esto es porque los comandos de duplicación de objetos (y clonación) no trabajan con recursividad, sino que hacen solo la duplicidad de los objetos inmediatos, por tanto, si se trata de un Array, se duplica como nuevo objeto el Array, pero cada elemento dentro del Array, si es a su vez otro Array o Hash, los elementos que este puede contener se dejan sin duplicar (o clonar).

Esto está así pensado para que cada cual agregue sus propias funciones de clonación (en caso de clone)… solo que en ese caso, para los objetos de tipo Array y Hash se les olvidó hacerlo, claro.

Solución cutre

Buscando un poco por internet, en varios foros se puede encontrar esta solución, la cual es algo chapuza en muchos aspectos:

irb> b = Marshal.load(Marshal.dump(a))
[[1,2,3], [1,2,3]]

Esto lo que hace es realizar una serialización de los objetos y después una deserialización. Funciona, pero no con todos los tipos de objetos, hay que tener especial cuidado con esto.

Solución algo más elegante

Lo ideal sería sobrecargar la función de clone para los objetos de Array y Hash, ya que se ve que se les olvidó hacerlo a los programadores o, realmente, no se preocuparon de hacer esa tarea en profundidad, es decir, a través de todos los objetos.

class Array
  def clone
    a = Array.new
    for i in ..(size - 1) do
      if self[i].respond_to? :clone
        a[i] = self[i].clone
      else
        a[i] = self[i]
      end
    end
    a
  end
end

Si se ejecuta con el ejemplo, se verá que se suceden algunos errores. Esto es debido a que los objetos como Fixnum, tienen implementado el objeto clone, pero como un error, ya que lógicamente se considera que el objeto Fixnum no se puede clonar.

Esto se puede resolver de dos formas. Agregar tantas excepciones como se encuentren en la función clone escrita antes, o escribir algo como esto:

class Fixnum
    def clone
      self
    end
end

Conclusión

Cada lenguaje tiene ciertas características o lagunas que, cuando se choca con ellas, se convierten en verdaderos escollos en el camino. No obstante, siempre se puede salir de ellos de alguna forma, aunque haya que poner momentáneamente FIXME en los comentarios de nuestro código a fin de revisarlo cuando se tenga una solución algo mejor.