Tengo esta matriz, en una consola ruby 1.8.6:

arr = [{:foo => "bar"}, {:foo => "bar"}]

Ambos elementos son iguales entre sí:

arr[0] == arr[1]
=> true
#just in case there's some "==" vs "===" oddness...
arr[0] === arr[1]
=> true 

Pero arr.uniq no elimina los duplicados:

arr.uniq
=> [{:foo=>"bar"}, {:foo=>"bar"}]

¿Alguien puede decirme qué está pasando aquí?

EDITAR: Puedo escribir un uniqifier no muy inteligente que use include? de la siguiente manera:

uniqed = []
arr.each do |hash|
  unless uniqed.include?(hash)
    uniqed << hash
  end
end;false
uniqed
=> [{:foo=>"bar"}]

Esto produce el resultado correcto, lo que hace que la falla de uniq sea aún más misteriosa.

EDICIÓN 2: Algunas notas sobre lo que está sucediendo, posiblemente solo para mi propia claridad. Como @ Ajedi32 señala en los comentarios, la falla en la unificación proviene del hecho de que los dos elementos son objetos diferentes. Algunas clases definen los métodos eql? y hash, usados para la comparación, para significar "son efectivamente lo mismo, incluso si no son el mismo objeto en la memoria". String hace esto, por ejemplo, por lo que puede definir dos variables para que sean "foo" y se dice que son iguales entre sí, aunque no sean el mismo objeto.

La clase Hash no hace esto, en Ruby 1.8.6, por lo que cuando se llama a .eql? y .hash en un objeto hash (el método .hash no tiene nada que ver hacer con el tipo de datos Hash (es como el tipo de suma de comprobación de hash) recurre a los métodos definidos en la clase base Object, que simplemente dicen "¿Es el mismo objeto en la memoria".

Los operadores == y ===, para objetos hash, ya hacen lo que quiero, es decir, decir que dos hashes son iguales si su contenido es el mismo. He anulado Hash#eql? para usar estos, así:

class Hash
  def eql?(other_hash)
    self == other_hash
  end
end

Pero, no estoy seguro de cómo manejar Hash#hash: es decir, no sé cómo generar una suma de verificación que será la misma para dos hashes cuyo contenido es el mismo y siempre diferente para dos hashes con diferentes contenido.

@ Ajedi32 sugirió que echara un vistazo a la implementación de Rubinius del método Hash#hash aquí https://github.com/rubinius/rubinius/blob/master/core/hash.rb#L589, y mi versión de la implementación de Rubinius se ve así:

class Hash
  def hash
    result = self.size
    self.each do |key,value|
      result ^= key.hash 
      result ^= value.hash 
    end
    return result
  end
end

Y esto parece funcionar, aunque no sé qué hace el operador "^ =", lo que me pone un poco nervioso. Además, es muy lento, aproximadamente 50 veces más lento según algunas evaluaciones comparativas primitivas. Esto puede hacer que su uso sea demasiado lento.

EDICIÓN 3: Un poco de investigación ha revelado que "^" es el operador OR exclusivo de Bitwise. Cuando tenemos dos entradas, un XOR devuelve 1 si las entradas son diferentes (es decir, devuelve 0 para 0,0 y 1,1 y 1 para 0,1 y 1,0).

Entonces, al principio pensé que eso significaba que

result ^= key.hash 

Es la abreviatura de

result = result ^ key.hash

En otras palabras, haga un XOR entre el valor actual del resultado y la otra cosa, y luego guárdelo como resultado. Sin embargo, todavía no entiendo la lógica de esto. Pensé que tal vez el operador ^ tenía algo que ver con punteros, porque llamarlo en variables funciona mientras que llamarlo en el valor de la variable no funciona: p.

var = 1
=> 1
var ^= :foo
=> 14904
1 ^= :foo
SyntaxError: compile error
(irb):11: syntax error, unexpected tOP_ASGN, expecting $end

Entonces, está bien llamar a ^ = en una variable pero no en el valor de la variable, lo que me hizo pensar que tiene algo que ver con la referencia / desreferenciación.

Las implementaciones posteriores de Ruby también tienen código C para el método hash # hash, y la implementación de Rubinius parece demasiado lenta. Poco atascado ...

3
Max Williams 14 nov. 2017 a las 19:36

2 respuestas

La mejor respuesta

Por razones de eficiencia, Array#uniq no compara valores usando == o incluso ===. Según los documentos:

Compara valores usando su hash y eql? métodos de eficiencia.

(Tenga en cuenta que vinculé los documentos para 2.4.2 aquí. Si bien los documentos para 1.8.6 no incluyen esta declaración, creo que todavía es válida para esa versión de Ruby).

En Ruby 1.8.6, ni Hash#hash ni Hash#eql? están implementados, por lo que recurren a Object#hash y Object#eql?:

Igualdad: en el nivel de objeto, == devuelve verdadero solo si obj y otros son el mismo objeto. Normalmente, este método se anula en las clases descendientes para proporcionar un significado específico de la clase.

[...]

El método eql? devuelve true si obj y anObject tienen el mismo valor. Utilizado por Hash para probar la igualdad de los miembros. Para objetos de la clase Object, eql? es sinónimo de ==.

Entonces, de acuerdo con Array#uniq, esos dos hashes son objetos diferentes y, por lo tanto, son únicos.

Para solucionar este problema, puede intentar definir {{X0} } y {{X1} } usted mismo. Los detalles de cómo hacer esto se dejan como ejercicio para el lector. Sin embargo, puede resultarle útil consultar implementación de estos métodos por parte de Rubinius.

2
Ajedi32 15 nov. 2017 a las 14:51

¿Qué tal usar JSON stringify y analizarlo como en Javascript?

require 'json'
arr.map { |x| x.to_json}.uniq.map { |x| JSON.parse(x) }

Es posible que los métodos json no sean compatibles con 1.8.6, utilice el que sea compatible.

0
Nandu Kalidindi 14 nov. 2017 a las 16:54