💡

[Bug #22013] Array#| で要素の合計が17以上の場合に #eql? で比較されないバグ報告

に公開

[Bug #22013] Array#| deduplication via eql? breaks when total element count exceeds ~16

# 配列の要素をまとめつつ、同じ要素は重複しないようにする
p [1, 3, 5, 7] | [1, 2, 3, 4, 5]
# => [1, 3, 5, 7, 2, 4]
  • また、各配列にすでに重複している要素もすべて取り除かれる
p [1, 1, 2, 2, 3, 3, 3] | [4, 4]
# => [1, 2, 3, 4]
  • この Array#| なのですが次のように #eql? メソッドを定義しているオブジェクトに対して『要素の合計が17以上の場合に正しく動作しない』というバグ報告になる
class Item
  attr_reader :id, :pos

  def initialize(id, pos:)
    @id = id
    @pos = pos
  end

  def inspect
    "item-#{id}-#{pos}"
  end

  # id でのみ要素を比較する
  def eql?(other)
    id == other.id
  end
end

# pos は違うが id は同じオブジェクトを用意する
items1 = [Item.new(1, pos: 1)]
items2 = [Item.new(1, pos: 2)]

pp items1.eql? items2 # => true

# 要素の合計数が 16以下の場合は意図する挙動になる
pp (items1 * 8) | (items2 * 8)
# => [item-1-1]

# 要素の合計数が 17以上の場合は重複する要素が取り除かれない
pp (items1 * 8) | (items2 * 9)
# 期待する値 => [item-1-1]
# 実際の値   => [item-1-1, item-1-2]
  • これなんですが、これだけみるとバグっぽいように見えるんですが実際には Array#| では #eql? だけではなくて #hash メソッドも適切に定義する必要があるようですね
  • なので次のように Item#hash メソッドを定義すると期待する挙動になる
class Item
  attr_reader :id, :pos

  def initialize(id, pos:)
    @id = id
    @pos = pos
  end

  def inspect
    "item-#{id}-#{pos}"
  end

  def eql?(other)
    id == other.id
  end

  # #hash メソッドも定義する
  def hash
    [self.class, id].hash
  end
end

items1 = [Item.new(1, pos: 1)]
items2 = [Item.new(1, pos: 2)]


# 要素の合計が17以上でも期待する結果になる
pp (items1 * 8) | (items2 * 9)   # => [item-1-1]
pp (items1 * 17) | (items2 * 17) # => [item-1-1]
GitHubで編集を提案

Discussion