Ruby で任意の異なるクラスのオブジェクトを #uniq するときの注意

元ネタ

と、いうことでちょっとやってみたら見事にハマったので覚書

元のコード

class Hoge
    def initialize(num)
        @num = num
    end
    def ==(other)
        if @num == other
            true
        else
            false
        end
    end
end

p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq
# => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 1, 2, 5, 6]

元のコードはユーザ定義クラスと Integer が入り混じった配列をいい感じに #uniq したいという内容でした。

#uniq の条件

#uniq は要素の Object#eql? を使用して重複を判定します。 また、 #eql? を定義した場合は Object#hash も再定義する必要があります。 と、いうことでこれに沿って修正してみました。

class Hoge
    def initialize(num)
        @num = num
    end

    def hash
        @num.hash
    end

    def eql?(other)
        if @num == other
            true
        else
            false
        end
    end
end

p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq
# => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 1, 2, 5, 6]

しかし、これでもまだ意図した動作になりません。

#hash#eql? は双方向で true になる必要がある

調べていたらわかったんですが

Hoge.new(1).eql? 1 # => true

だけではなくて

1.eql? Hoge.new(1).eql? # => true

のように双方向で true になる必要があるみたいです(当然 #hash に関しても同様

完成

と、言うことで Integer#eql? を拡張することで無事に意図する動作になりました。

class Hoge
    def initialize(num)
        @num = num
    end

    def hash
        @num.hash
    end

    def eql?(other)
        if @num == other
            true
        else
            false
        end
    end
end

class Integer
    def eql?(other)
        return super(other) unless Hoge === other
        other.eql? self
    end
end

_1 = Hoge.new(1)

# この条件をすべて満たす必要がある
p _1.hash == 1.hash
p _1.eql? 1
p 1.hash == _1.hash
p 1.eql? _1

p [Hoge.new(1), Hoge.new(2), 1, 2, 5 ,6].uniq
# => [#<Hoge:0x000000024871f8 @num=1>, #<Hoge:0x000000024871d0 @num=2>, 5, 6]

流石に #uniq のためだけに Integer をクラス拡張するのはツラいですね…。

まとめ

  • #uniq#eql? を参照して重複のチェックを行う
  • #eql? を書き換えた場合は #hash も書き換える必要がある
  • #uniq は双方向でチェックするので要素すべてのオブジェクトに対して #eql? を考慮する必要がある