mikmak
mikmak

Reputation: 13

Ruby set with custom class

I'm trying to correctly implement some custom classes in order to use them in a set. Custom class B contains an array instance variable that holds objects of class A. Here's a MWE:

#!/usr/bin/env ruby

require 'set'

class A
  attr_reader :a

  def initialize(a)
    @a = a
  end

  def hash
    @a.hash
  end

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

class B
  attr_reader :instances

  def initialize
    @instances = Array.new
  end

  def add(i)
    @instances.push(i)
  end

  def hash
    @instances.hash
  end

  def eql?(other)
    @instances == other.instances
    #@instances.eql?(other.instances)
  end
end

s = Set.new

b1 = B.new
b1.add(A.new(4))
b1.add(A.new(5))

b2 = B.new
b2.add(A.new(4))
b2.add(A.new(5))

s.add(b1)
s.add(b1)
s.add(b2)
puts s.size

The output is 2, expected is 1 since b1 and b2 are objects constructed with the same values.

If I use eql? instead of == in the implementation of eql? in class B, then the output is correct. According to the definition of == in the ruby docs, shouldn't the use of == be correct here? Where is my error in understanding this?

Upvotes: 1

Views: 794

Answers (1)

Stefan
Stefan

Reputation: 114188

TL;DR it's because:

  • Array#== compares elements via ==
  • Array#eql? compares elements via eql?

According to the definition of == in the ruby docs, shouldn't the use of == be correct here?

If you compare the arrays via Array#== the corresponding items will also be compared via ==: (emphasis added)

Two arrays are equal if they contain the same number of elements and if each element is equal to (according to Object#==)

Example:

[A.new(4)] == [A.new(4)]
#=> false

it fails because the elements are not equal according to ==:

A.new(4) == A.new(4)
#=> false

If I use eql? instead of == in the implementation of eql? in class B, then the output is correct.

Array#eql? works, because it compares corresponding items via eql?: (emphasis added)

Returns true if self and other are the same object, or are both arrays with the same content (according to Object#eql?).

Example:

[A.new(4)].eql? [A.new(4)]
#=> true

because of:

A.new(4).eql? A.new(4)
#=> true

In order to get == working, you have to implement A#==:

class A
  # ...
  def eql?(other)
    @a == other.a
  end
  alias_method :==, :eql?
end

A.new(4) == A.new(4)     #=> true
[A.new(4)] == [A.new(4)] #=> true

Upvotes: 2

Related Questions