Four Types of Equality in Ruby

2013-05-27

Ruby provides four methods to test for various types of equality. The methods are namely equal?, ==, ===, and eql?. Distinguishing them is a perennial question for newcomers to Ruby.

As one of numerous details worth knowing about Ruby, the distinction between each method has taken me some time to digest. Recently, though, I have been reading through Eloquent Ruby, my second favorite Ruby book after The Well-Grounded Rubyist, and finally reached a point where the differences are clear.

First comes the most important method of all, equal?. Whereas the other methods occasionally require overriding, equal? is sacrosanct. The other methods of equality are built on top of equal? in one way or another. The documentation is worth consulting to see how this works in C. In effect, equal? is comparing pointers to objects, which amounts to comparing the object_id of two objects in Ruby. If two variables point to the same object, (i.e., their object_id is the same), then equal? will return true.

"string".equal? "string" # => false, because each string is a separate object

After equal? comes ==. Unlike equal?, the double-equals method is often overridden to provide more meaningful object comparison. Consider the following:

class Car
  attr_reader :make
  def initialize(make)
    @make = make
  end

  def ==(other)
    make == other.make
  end
end

foo_car = Car.new("Toyota")
bar_car = Car.new("Toyota")

foo_car.equal? bar_car # => false, they are two distinct objects
foo_car == bar_car     # => true,  on account of their common make

Without overriding ==, a comparison between two cars would test their object_id, when we perhaps consider two cars to be "equal" when they both have the same make.

Next is === (threequals) which calls == (double-equals) by default. The Regexp class provides a good example of overriding the threequals method. In other words,

/c.t/ == "cat"   # => false, because we have two separate objects
/c.t/ === "cat"  # => true,  because the Regexp matches the string

The case statement evalutes equality in terms of ===, and so provides yet another context in which overriding threequals might make sense. Likewise, the Date class overrides the method to test whether two dates fall on the same day (irrespective of each object being unique).

Finally, we have eql?. The eql? method is used by the Hash class to compare keys. In other words, Hash uses eql? to test whether two keys are the same. By defining the behavior of eql? and the hash method in a class, we can implement our own custom behavior for storage in a hash.

Consider the following contrived example. We start with a simple class modeling a key.

class Key
  attr_reader :type
  def initialize(type)
    @type = type
  end
end

And then we create two instances of that key and store one in a hash called doors.

key_one = Key.new('skeleton')
key_two = Key.new('skeleton')

doors = {}
doors[key_one] = :secret_room

As it stands now, if we lookup key_two in our doors hash, we will not get the same result as key_one even though both keys are equally skeleton keys.

doors[key_two] # => nil

To achieve our desired behavior, we need to do two things. First, we need to override eql? which currently tests keys with only their object_id. Second, we need to override the hash method, which currently hashes an object based on its object_id.

class Key
  def eql?(other)
    type == other.type
  end

  def hash
    type.hash
  end
end

Now when we recreate our doors hash and perform the same assignment and lookup, we will get our desired behavior. All skeleton keys will gain access to the secret room.

doors = {}
doors[key_one] = :secret_room
doors[key_two] # => :secret_room

Aside from equal?, the equality methods are built into Ruby to provide custom behavior and are meant to be overrided. Out of the box, the equality methods are all implemented in terms of equal?, aside from a number of core classes whose behavior is easy to take for granted (e.g., "a" == "a", or 1 == 1.0).