Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does the Ruby `uniq` method use for equality checking?

Tags:

ruby

I'm interested in implementing a custom equality method for use in an array of objects in Ruby. Here's a stripped-back example:

class Foo

  attr_accessor :a, :b

  def initialize(a, b)
    @a = a
    @b = b
  end 

  def ==(other)
    puts 'doing comparison'
    @a == @a && @b == @b
  end

  def to_s
    "#{@a}: #{@b}"  
  end

end 

a = [
  Foo.new(1, 1),
  Foo.new(1, 2),
  Foo.new(2, 1),
  Foo.new(2, 2),
  Foo.new(2, 2)
]
a.uniq

I expected the uniq method to call Foo#==, and remove the last instance of Foo. Instead, I don't see the 'doing comparison' debug line and the array remains the same length.

Notes:

  • I'm using ruby 2.2.2
  • I've tried defining the method as ===
  • I have done it long-hand with a.uniq{|x| [x.a, x.b]}, but I don't like this solution it's making the code look pretty cluttered.
like image 370
AJFaraday Avatar asked Jul 16 '26 06:07

AJFaraday


1 Answers

It compares values using their hash and eql? methods for efficiency.

https://ruby-doc.org/core-2.5.0/Array.html#method-i-uniq-3F

So you should override eql? (that is ==) and hash

UPDATE:

I cannot explain fully why is that, but overriding hash and == doesn't work. I guess it's cause by the way uniq is implemented in C:

From: array.c (C Method): Owner: Array Visibility: public Number of lines: 20

static VALUE
rb_ary_uniq(VALUE ary)
{
    VALUE hash, uniq;

    if (RARRAY_LEN(ary) <= 1)
        return rb_ary_dup(ary);
    if (rb_block_given_p()) {
        hash = ary_make_hash_by(ary);
        uniq = rb_hash_values(hash);
    }
    else {
        hash = ary_make_hash(ary);
        uniq = rb_hash_values(hash);
    }
    RBASIC_SET_CLASS(uniq, rb_obj_class(ary));
    ary_recycle_hash(hash);

    return uniq;
}

You can bypass that by using a block version of uniq:

> [Foo.new(1,2), Foo.new(1,2), Foo.new(2,3)].uniq{|f| [f.a, f.b]}
=> [#<Foo:0x0000562e48937cc8 @a=1, @b=2>, #<Foo:0x0000562e48937c78 @a=2, @b=3>]

Or use Struct instead:

F = Struct.new(:a, :b)
[F.new(1,2), F.new(1,2), F.new(2,3)].uniq
# => [#<struct F a=1, b=2>, #<struct F a=2, b=3>]

UPDATE2:

Actually in terms of overriding it's not the same if you override == or eql?. When I overriden eql? It worked as intended:

class Foo
  attr_accessor :a, :b

  def initialize(a, b)
    @a = a
    @b = b
  end 

  def eql?(other)
    (@a == other.a && @b == other.b)
  end

  def hash
    [a, b].hash
  end

  def to_s
    "#{@a}: #{@b}"  
  end

end 

a = [
  Foo.new(1, 1),
  Foo.new(1, 2),
  Foo.new(2, 1),
  Foo.new(2, 2),
  Foo.new(2, 2)
]
a.uniq
#=> [#<Foo:0x0000562e483bff70 @a=1, @b=1>,
#<Foo:0x0000562e483bff48 @a=1, @b=2>,
#<Foo:0x0000562e483bff20 @a=2, @b=1>,
#<Foo:0x0000562e483bfef8 @a=2, @b=2>]
like image 191
mrzasa Avatar answered Jul 19 '26 01:07

mrzasa