Comparing custom objects in Ruby using uniq.
by Rabbit
You author a Client model. You have an array of clients and you want to call Ruby’s uniq method on them. You define “unique” clients as having the same name. This doesn’t work:
>> c1 = Client.find(1) => #<Client id: 1, artist_id: 1, name: "Daniel Waite", alias: "Rabbit", birthdate: nil, created_at: "2008-01-13 13:20:14", updated_at: "2008-01-23 01:03:26"> >> c2 = Client.find(4) => #<Client id: 4, artist_id: 1, name: "Daniel Waite", alias: nil, birthdate: nil, created_at: "2008-01-14 23:44:58", updated_at: "2008-01-14 23:44:58"> [ c1, c2 ].uniq.size # 2
Being the clever Ruby programmer you are, you quickly modify client…
class Client < ActiveRecord::Base def ==(other) name == other.name end end
Satisfied, you run the above code again.
[ c1, c2 ].uniq.size # 2
What the fuck? Frustrated, you go medieval…
class Client < ActiveRecord::Base def ==(other) name == other.name end def ===(other) name == other.name end def eql?(other) name == other.name end def equal?(other) name == other.name end end
Huffing and puffing, you carefully run your code again…
[ c1, c2 ].uniq.size # 2
God damn it!
But fear not my friend, for you have sought, and so the answer shall be revealed to you…
class Client < ActiveRecord::Base def hash name.hash end def eql?(other) name == other.name end end
Wearily, you try again…
[ c1, c2 ].uniq.size # 1
Success!
Yes, both methods must be overridden.
You may have never seen it, but the hash method is available to all objects. I’m not quite sure how or why this works, but it does. Rejoice.
**UPDATE**
Russ pointed out that for new or unsaved objects, yes, all you have to override is the eql? method. However, to compare for equality for existing objects (i.e. objects retrieved from the database) you must override both methods. This is because Rails uses the ID attribute of your models to test for equality, and all unsaved models have an ID attribute of nil, so of course nil == nil.
This makes things more interesting, too. If you wanted to define equality between clients as having both the same name and the same birthdate, you can write…
class Client < ActiveRecord::Base def hash %{#{ name }#{ birthdate }}.hash end def eql?(other) hash == other.hash end end
And now you have a more complex definition of what equality means. Gotta love Ruby.
Comments
You only have to override the eql? method. :D
The hash method is part of ActiveRecord and is defined as:
def hash
id.hash
end
The hash method that is part of all objects creates a sort of checksum of the value passed.
Interesting information on the #hash method. However, we’re both right and both wrong. I should know better than to type code directly into WordPress… (My example code differs from the code I was using in console.)
The example code as I have it works if all you define is eql?. However, if you find existing clients, each with a unique ID, it doesn’t.
It works with new objects because nil.hash is the same as nil.hash.
It doesn’t work with found objects because 1.hash is not 2.hash.
We override the name attribute of Client because “Rabbit”.hash is the same as “Rabbit”.hash.
Thanks for pointing that out. =)
I’ll edit the post to reflect the new information.
Very entertaining.
DUDE! Nice. This clears up some stuff too.