Ruby 1.8.6 Array#uniq не удаляет повторяющиеся хэши

У меня есть этот массив в консоли ruby ​​1.8.6:

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

оба элемента равны друг другу:

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

Но arr.uniq не удаляет дубликаты:

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

Кто-нибудь может сказать мне, что здесь происходит?

РЕДАКТИРОВАТЬ: я могу написать не очень умный uniqifier, который использует include? следующим образом:

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

Это дает правильный результат, что делает отказ uniq еще более загадочным.

РЕДАКТИРОВАТЬ 2: Некоторые заметки о том, что происходит, возможно, просто для моей ясности. Как указывает @Ajedi32 в комментариях, отказ от uniqify происходит из-за того, что два элемента являются разными объектами. Некоторые классы определяют методы eql? и hash, используемые для сравнения, означающие, что "действительно это одно и то же, даже если они не являются одним и тем же объектом в памяти". Например, это делает String, поэтому вы можете определить две переменные как «foo», и говорят, что они равны друг другу, даже если они не являются одним и тем же объектом.

Класс Hash не делает этого в Ruby 1.8.6, поэтому, когда .eql? и .hash вызываются для хэш-объекта (метод .hash не имеет ничего общего с типом данных Hash — он как хеш контрольной суммы) он возвращается к использованию методов, определенных в базовом классе Object, которые просто говорят: «Это тот же самый объект в памяти».

Операторы == и === для хеш-объектов уже делают то, что я хочу, то есть говорят, что два хэша одинаковы, если их содержимое одинаково. Я переопределил Hash#eql?, чтобы использовать их, например:

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

Но я не знаю, как обращаться с Hash#hash: то есть я не знаю, как сгенерировать контрольную сумму, которая будет одинаковой для двух хэшей с одинаковым содержимым и всегда разной для двух хэшей с разным содержимым.

@Ajedi32 предложил мне взглянуть на реализацию метода Hash#hash Рубиниусом здесь https://github.com/rubinius/rubinius/blob/master/core/hash.rb#L589 , и моя версия реализации Rubinius выглядит так:

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

и это, похоже, работает, хотя я не знаю, что делает оператор "^=", что меня немного нервирует. Кроме того, он очень медленный — примерно в 50 раз медленнее на основе некоторых примитивных тестов. Это может сделать его слишком медленным в использовании.

РЕДАКТИРОВАТЬ 3: Небольшое исследование показало, что "^" является оператором побитового исключающего ИЛИ. Когда у нас есть два входа, XOR возвращает 1, если входы разные (т.е. он возвращает 0 для 0,0 и 1,1 и 1 для 0,1 и 1,0).

Итак, сначала я подумал, что это означает, что

result ^= key.hash 

является сокращением для

result = result ^ key.hash

Другими словами, выполните XOR между текущим значением результата и другим значением, а затем сохраните его в результате. Я все еще не совсем понимаю логику этого, хотя. Я подумал, что, возможно, оператор ^ как-то связан с указателями, потому что его вызов для переменных работает, а вызов для значения переменной не работает: например

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

Итак, это нормально с вызовом ^= для переменной, но не для значения переменной, что заставило меня подумать, что это как-то связано со ссылкой/разыменованием.

Более поздние реализации Ruby также имеют код C для метода Hash#hash, и реализация Rubinius кажется слишком медленной. Немного застрял...


person Max Williams    schedule 14.11.2017    source источник
comment
Ruby 1.8.6 снова был выпущен более 10 лет назад, 9 лет назад была выпущена обновленная 1.8.7 версия, а все 1.8.x версии достигли конца жизни более 4 лет назад. Почему тебя это вообще волнует?   -  person spickermann    schedule 14.11.2017
comment
Почему кого-то волнуют старые версии чего-либо? Потому что они вынуждены использовать их с устаревшими сайтами.   -  person Max Williams    schedule 14.11.2017
comment
Работать с устаревшими приложениями — это одно, но работать с приложением, которое использует версию, устаревшую более 9 лет. Вау, это смешно...   -  person spickermann    schedule 14.11.2017
comment
@spickermann здесь нет аргументов   -  person Max Williams    schedule 15.11.2017


Ответы (2)


По соображениям эффективности Array#uniq не сравнивает значения с использованием == или даже ===. Согласно документам:

Он сравнивает значения, используя их хэш и eql? методы повышения эффективности.

(Обратите внимание, что я связал документы для 2.4.2 здесь. Хотя документы для 1.8.6 не включают это утверждение, я считаю, что оно все еще верно для этой версии Ruby.)

В Ruby 1.8.6 ни Hash#hash, ни Hash#eql? не реализованы, поэтому они возвращаются к использованию Object#hash и Object#eql?:

Равенство — на уровне объекта == возвращает значение true, только если obj и other являются одним и тем же объектом. Как правило, этот метод переопределяется в классах-потомках, чтобы обеспечить специфичное для класса значение.

[...]

Метод eql? возвращает true, если obj и anObject имеют одинаковое значение. Используется Hash для проверки членов на равенство. Для объектов класса Object eql? является синонимом ==.

Итак, согласно Array#uniq, эти два хеша являются разными объектами и, следовательно, уникальны.

Чтобы исправить это, вы можете попробовать определить Hash#hash и Hash#eql? самостоятельно. Детали того, как это сделать, оставляем читателю в качестве упражнения. Однако вам может быть полезно обратиться к реализации этих методов в Rubinius.

person Ajedi32    schedule 14.11.2017
comment
Ах, да: arr[0].eql?(arr[1]) возвращает false, так что похоже, что это ключ. Спасибо! - person Max Williams; 14.11.2017
comment
В качестве дополнительного вопроса: как вы думаете, как лучше всего определить eql? для Hash, чтобы он вел себя так, как ожидалось? (т.е. проверить, идентично ли содержимое). Я думал, что могу просто сделать self == other_hash, но почему-то это кажется проблемой... - person Max Williams; 14.11.2017
comment
@MaxWilliams Я бы, наверное, проверил, одинаковы ли класс и длина каждого хэша, затем перебрал бы каждый ключ и значение, используя Hash#each_pair, и сравнил бы их с eql?. (Хотя я не уверен, что порядок имеет значение?) Вам также может понадобиться определить Hash#hash, используя аналогичный подход. - person Ajedi32; 14.11.2017
comment
На самом деле @ Ajedi32, я думаю, что .eql? все-таки не ключ. Я добавил расширение к Hash, чтобы метод eql? просто выполнял self == other_hash, и теперь я получаю arr[0].eql?(arr[1]) => true. Отлично. Однако uniq по-прежнему сохраняет дубликаты, что заставляет меня думать, что uniq в конце концов не использует eql?. Согласно моей документации по API, исходный код uniq выглядит как код C. - person Max Williams; 14.11.2017
comment
ах извините, я пропустил это немного. Hash#hash, как запутанно :) - person Max Williams; 15.11.2017
comment
Я написал большую (извините) пару правок к моему посту о методе Hash#hash, не могли бы вы прочитать его? Я могу скопировать (я думаю) метод Рубиния Hash#hash, но я не понимаю некоторых из них. Моя версия очень медленная, и я не знаю, потому ли это, что я сделал ошибку, переводя ее с Рубиния... - person Max Williams; 15.11.2017
comment
@MaxWilliams Да, это довольно долго. Лично я бы предложил разделить это на пару новых вопросов; один о том, что делает оператор ^=, и, возможно, еще один на codereview.stackexchange.com о том, как улучшить производительность существующего кода. Свяжите их здесь, когда они будут готовы, чтобы я мог взглянуть на них; это, безусловно, кажется интересной проблемой. - person Ajedi32; 15.11.2017

Как насчет использования JSON stringify и парсинга, как в Javascript?

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

Методы json могут не поддерживаться в 1.8.6, пожалуйста, используйте тот, который когда-либо поддерживался.

person Nandu Kalidindi    schedule 14.11.2017