Well-behaved Ruby Objects: Equality

A “well-behaved” object in Ruby needs to understand the following:

What makes two Ruby objects “equal”?
And which version of “equal” (there are several in Ruby)?
And what makes an object usable as a Hash key?
And is that the same thing that makes them Comparable?

Because I can never seem to remember the specifics. And because my searching the Interwebs seems to find related, but not specific help. And because I’ve got this blurgh-thing… I might as well use it to help future-me (and maybe you?).

Gimme the gist

Implement hash and eql? for use as a Hash key, and then alias the eql? method to == for the expected developer ergonomics. Something like this:

class Message
  attr_reader :body, :subject

  def initialize(subject:, body:)
    @subject = subject
    @body = body
  end

  def eql?(other)
    other.class == self.class &&
      other.body == body &&
      other.subject == subject
  end
  alias == eql?

  def hash
    [self.class, body, subject].hash
  end
end

What to know more about the specifics, or how to also make these objects Comparable? Read on, friend…

Surely someone else has covered this already

As it turns out, Russ Olsen covers this very topic, in depth and with great examples, in his book, Eloquent Ruby. I should know that because I’ve read that book, and recommend it! However, it always takes me several trips to The Googles to find a snippet which eventually points me back to that book. At which point I have to dig out my copy. All of this only after I’ve tried a dozen other search terms resulting in articles about the difference between the many forms of equality in Ruby.

So yes, to various degrees, it’s been covered before. There is nothing new under the Sun.

Like I said, I have this here blurgh and I’m gonna use it. 🤷

Every-day object equality

For every-day use, the form of object equality we most often use in Ruby is the ==, or double-equals, form. By default, the == operator is an alias of the equal? method, which it inherits from Object. This form compares the object identity, effectively asking “are these the same object in memory?”

We don’t want this form, so we need to override it to something more meaningful for our object. Often two objects are ==, or double-equals equal, if they are of the same class, and some set of their internal data is also ==. In the case above, that means both objects are a Message, and their body and subject are the same.

def ==(other)
  other.class == self.class &&
    other.body == body &&
    other.subject == subject
end

Please go read Mr. Olsen’s excellent coverage of the topic for more details and in-depth examples.

Use as a Hash key

When used as a Hash key, Hash will call the object’s hash method to get hash code (an Integer value) to use when storing and searching for the key. It then uses the object’s eql? method to determine if two hash keys are the same key. From the Ruby docs:

hash → integer

Generates an Integer hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.

The hash value is used along with eql? by the Hash class to determine if two objects reference the same hash key. Any hash value that exceeds the capacity of an Integer will be truncated before being used.

The hash value for an object may not be identical across invocations or implementations of Ruby. If you need a stable identifier across Ruby invocations and implementations you will need to generate one with a custom method.

By default, objects inherit their hash and eql? methods from Object, which compares the object identity, similar to equal?. As you might guess, we don’t want that for our object.

For hash, we can lean on Ruby already knowing how to calculate the hash code for an Array of values. We’ll use all of the values composing the identity of the object itself.

def hash
  [self.class, body, subject].hash
end

To determine if two objects are eql?, and hence, the same keys in a hash, we want to make sure they’re of the same class, and some set of their internal data is also ==.

def eql?(other)
  other.class == self.class &&
    other.body == body &&
    other.subject == subject
end

Look familiar? It’s the same as the == from above!

In practice, I often implement eql? and hash since they are a bit of a duo - one requiring the other. And then I alias == to be the same as eql?

def eql?(other)
  other.class == self.class &&
    other.body == body &&
    other.subject == subject
end
alias == eql?

And that’s it, these objects now know if they are everyday-equal, and can serve as Hash keys. We’ve not covered === here; the links above provide explanations about when, why, and how you might need to reach for that form of equality amongst Ruby objects. Again, go check out Mr. Olsen’s book; it’s great!

What about Comparable?

Being able to compare, and thus sort two Ruby objects is a bit tangential to the forms of equality we’ve discussed here.

Objects with a “natural ordering” will implement the <=> or spaceship operator. This operator is used to determine if one object is less than the other, greater than the other, or equal to the other. Ruby’s Comparable module will use that to implement several comparison operators. And all of that can be use by other Ruby modules, like Enmerable to do things like sort a collection.

🤓 Neat!

Again, Mr. Olsen’s book, or any number of other articles, including the Ruby docs for Comparable, cover this in great detail.

The gist, once more

To recap, implement the hash and eql? methods to use an object as a Hash key. Then alias the eql? method to == to cover they typical, “every day”, usage we expect when writing/reading Ruby.