Read more

Ruby object equality

Avatar
Daniel Straßner
January 13, 2023Software engineer at makandra GmbH

TLDR

if you define a equality method for a class you must also implement def hash.

Ruby has a lot of methods that have to do something with equality, like ==, ===, eql?, equal?. This card should help you differentiate between those and give you hints on how to implement your own equality methods in a safe manner.

Differences between the methods

for everyday use: ==

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more

When you compare two objects in ruby, you most often see the use of foo == bar. By default the == operator inherits from Object and is implemented as object identity (see equal? below).

If you want to give this method more meaning for your class you can override this. For a PORO (Plain Old Ruby Object) that holds some attributes it is pretty common to compare the equality of all attributes instead:

def ==(other)
  other.instance_of?(self.class) &&
    other.street == street &&
    other.zip == zip
end

for hash equality: eql?

The eql? method is closely connected to the hash method. In ruby any object can be used as a key in hashes (using the hash rocket notation a_hash = { obj => 'some value' }).

To find objects by keys (e.g. a_hash[obj]), the keys are not compared using one of the discussed equality methods, but by comparing the values of the hash method.
hash must return an integer that is equal for objects that are equal according to the eql? method. This makes comparison cheaper (and allows hashes to be organized efficiently in a hash table).

Thus we have to remember to always override hash when eql? is overridden. Also methods like Array#uniq rely on this to be implemented correctly.

Overriding eql? is not enough to get hash equality right:

class Person
  attr_accessor :name
  
  def initialize(name)
    self.name = name
  end
  
  def eql?(other) # not enough for hash equality
    other.name == name
  end
end

bob = Person.new('bob')

phone_list = {
  bob => '0821 123',
}

phone_list[bob] # returns '0821 123'
phone_list[Person.new('bob')] # returns nil

[bob, Person.new('bob')].uniq
[
    [0] #<Person:0x0000559054263420 @name="bob">,
    [1] #<Person:0x000055905404ee50 @name="bob">
]

Only after implementing hash we get the expected results:

class Person
  ...
  def hash
    name.hash
  end
end

bob = Person.new('bob')

phone_list = {
  bob => '0821 123',
}

phone_list[bob] # returns '0821 123'
phone_list[Person.new('bob')] # now also returns '0821 123'

[bob, Person.new('bob')].uniq
[
    [0] #<Person:0x000055bdd7167ae8 @name="bob">
]

In most cases it makes sense to alias eql? to ==. This, however, is not always the case. Numeric types, for example, perform type conversion across ==, but not across eql?:

1 == 1.0     # true
1.eql? 1.0   # false

for case statements: ===

The triple equals operator === is used for case-equality. In general it tells if the object on the right "belongs to" or "is a member of" the object on the left:

String === "hello" # true
Range === (1..10) # true
Integer === 3.0 # false
("a".."f") === "d" # true

It is mainly used (implicitly) in case statements:

case host
when Host
  # ...
when /www\..*/
  # ...
end 

Your custom classes may safely override === to provide meaningful semantics in case statements.

for object identity: equal?

The equal? method is not one that you use very frequently as it checks for object identity. For

x.equal?(y)

to be true, both x and y need to reference the identically same object. You should never need to override equal?.


Recommendations for overriding equality methods

  • if you never compare objects for equality, use them as hash keys or in arrays, don't override equality methods at all
  • if you implement a wrapper class, consider using equality methods of the wrapped classes instead
  • in most cases eql? and == should be aliased
  • when you use your class as hash keys or arrays override eql?
  • whenever eql? is overridden, override hash accordingly

Here is an example implementation for a basic class that holds two attributes:

class Event
  attr_accessor :title, :date

  def eql?(other)
    # check that class is right
    return false unless other.instance_of?(self.class)
    
    # check that all attributes are equal
    title == other.title && date == other.date
  end
  # make == and eql? to be the same
  alias_method :==, :eql?

  def hash
    # calculate hash based on all attribute hashes
    title.hash ^ date.hash
  end
end

An even more generic implementation might look like this:

def hash
  hash_attributes.hash
end

def eql?(other)
  return false unless other.instance_of?(self.class)

  other.send(:hash_attributes) == hash_attributes
end
alias_method :==, :eql?

private

def hash_attributes
  [title, date]
end

Further reading:

Avatar
Daniel Straßner
January 13, 2023Software engineer at makandra GmbH
Posted by Daniel Straßner to makandra dev (2023-01-13 15:52)