Array#uniq と Object#eql? と Object#hash

常識なのかもしれないけど、ちょっとだけはまったので、メモ。

Array#uniq は配列から重複した要素を取り除いた新しい配列を返すメソッド。

["hoge", "fuga", "piyo", "piyo", "hoge", "fuge"].uniq
#=> ["hoge", "fuga", "piyo", "fuge"]

ただし、自前で定義したクラスのオブジェクトについては、そのままじゃうまく動いてくれない。

class Vector2D
  attr_reader :x, :y
  def initialize(x, y)
    @x, @y = x, y
  end
  def inspect
    "#<#{@x}, #{@y}>"
  end
end
ary = [[3, 4], [4, 3], [1, 2], [3, 4], [1, 2]].map{|x, y| Vector2D.new(x, y)}
#=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>]
ary.uniq
#=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>]

Array#uniq の

要素の重複判定は、Object#eql? により行われます。

また、Object#eql?

クラスの性質に合わせて再定義すべきです。多くの場合、 == と同様に同値性の判定をするように再定義されています

ということなので、この場合、Vector2D#eql? を定義してやればおっけー?

class Vector2D
  def eql?(other)
    @x.eql?(other.x) && @y.eql?(other.y)
  end
end

ちゃんと同値判定ができるようになりました。

ary[0].eql?(ary[1])
#=> false
ary[0].eql?(ary[3])
#=> true

が、

ary.uniq
#=> [#<3, 4>, #<4, 3>, #<1, 2>, #<3, 4>, #<1, 2>]

かわらんなぁ…。

実は Object#eql? には、こんな注意書きもあったのですが、まぁ、今回は関係ないよね、とか思ってたのでした。

Hash で二つのキーが等しいかどうかを判定するのに使われます。

(中略)

このメソッドを再定義した時には Object#hash メソッドも再定義しなければなりません。

Vector2D#hash を定義してやれば期待通りに動きます。Array#uniq の実装で Hash を使ってるのかな?

なお、Object#hash は

A.eql?(B) ならば A.hash == B.hash

の関係を必ず満たしていなければいけません。

ということですが、この場合は

class Vector2D
  def hash
    [@x, @y].hash
  end
end

これでおっけー。

ary.uniq
#=> [#<3, 4>, #<4, 3>, #<1, 2>]

できました。