One important concept in object-oriented languages is the difference between equality and identity. The concept isn’t complicated, but the English words we use to talk about it are imprecise. The thesaurus says that “identity” and “equality” are synonyms. So let’s back up and make sure we’re being clear on the concepts.
We often talk about variables as if they “hold” a certain object, but that’s not a very good metaphor. You can put the same object “inside” any number of different variables. If we’re working with containment as a metaphor, we run into problems here. It’s as if we’re saying that the same person is in two different houses at the same time.
Instead, we need to think of variables as names. Different people use different names for the same thing (Puma, Mountain Lion, Cougar) or the same name for multiple things (my “home” is not the same house as your “home”). Names are just a reference to something real, a way to address things.
We say that two variables are identical when they both refer to the same object. If they refer to different objects that represent the same value, we say that they are equal. Consider, for example two points that have the same X and Y coordinates. They are equal, but not identical. Objects themselves can tell you whether they’re identical or equal.
In Smalltalk, the relevant messages are:
||The objects are equal|
||The object(s) are identical|
||A number that must the same for all equal objects, typically used by collections|
The Smalltalk virtual machine is the sole place where object identity can be determined, so the #== method is implemented as a primitive and never overridden by any class. But scores of classes implement their own method for #= and #hash. A classic Smalltalk programming mistake is to override #= but not #hash, thus breaking the requirement that two equal objects have the same hash value.
In Ruby, there are actually significantly more equality messages that you may encounter. The definitions I’m using here come from pp. 95 and 571 of Programming Ruby:
||Test for equal value|
||Used to compare each of the items with the target in the when clause of a case statement|
||General comparison operator. Returns -1, 0, or +1, depending on whether its receiver is less than, equal to, or greater than its argument.|
||True if the receiver and the argument have both the same type and equal values. 1 == 1.0 returns true, but 1.eql?(1.0) returns false.|
||True if the receiver and argument have the same object ID|
||Generates a Fixnum hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.|
This is a little overwhelming. Ruby’s :equal? method is a test for identity, and like Smalltalk, it is implemented in Object and never overridden. That gets us started.
The methods for :eql? and :hash are probably the most similar to Smalltalk. If you override one, you need to override the other. Ruby’s collections use them to determine equality for things like Array#uniq or Hash lookup keys.
This has bitten me more than once. I’ve implemented a new sort of object and overridden ==, only to find later that
- I can’t use those objects for Hash keys, and
- Arrays return unexpected results when sent :uniq
It seems that instead of overriding ==, I should have overridden eql? and hash. This makes my Hashes and Array#uniq results work like I expected. But wait… The default Ruby implementation of == defers to equal?, which is not what I want either:
>> g1 = GridAddress.new(:key)
=> #<GridAddress:0xb7b569c8 @key=":key," @row="nil">
>> g2 = GridAddress.new(:key)
=> #<GridAddress:0xb7b50028 @key=":key," @row="nil">
>> g1.hash == g2.hash
>> g1 == g2
There’s no deferral from one of these messages to another. In other words, the behavior I want requires me to override eql?, hash, and ==. These are independent, parallel implementations. The difference is primarily semantic. With eql, there is no attempt to coerce the objects to a similar type. Methods for ==, on the other hand, typically try to coerce objects to the same types first before making comparisons.
Finally, we come to the “spaceship” operator, <=>. This is used by a mixin called Comparable, and you automatically get various other comparison operators (including ==) if you include Comparable and implement <=>. The only objects that understand it are those for which less-than and greater-than comparisons actually make sense.
In my opinion, Ruby complicates things here with little payoff. Most programs compare using ==, and the details of other equality comparisons are lost on most Ruby programmers. I think it’s a fair generalization that most Ruby developers don’t use their own objects as hash keys, probably due to a sort of bias towards the more “primitive” object types like Ramon Leon talks about (see “Obsession with Simple Types” in this post).
It’s also worth noting that if you override eql? or ==, you’re expected to check to make sure that the objects have the same type before you start comparing any details. This is the common pattern in Smalltalk, too. Most Smalltalk implementations of #= first check in some way to see that the two objects are the same kind of thing and then proceed to compare relevant details.