The key to Ruby hashes is eql? hash

I've been struggling for a few hours with key hashes in Ruby. We usually use primitive values as keys but what happens when we want to use one of our objects.

Yesterday I spent a couple hours trying to resolve this little Ruby riddle:

class Key
  attr_reader :value

  def initialize(value)
    @value = value
  end
end

k1 = Key.new('foo')
k2 = Key.new('foo')

h = { k1 => 'bar' }
puts h.has_key? k1 # -> true
puts h.has_key? k2 # -> false ??

I have been programming in Ruby for more than a year now. I have used the Hash class a myriad of times. But the thing is I’ve only used it with primitive values as keys. Usually a String or Symbol.

Yesterday I tried to use an instance of a class I just defined as a key of the hash. The first time, when calling the has_key? method I wasn’t surprised. My Java past surfaced and equals pop into my mind. So I made the class Comparable.

class Key
  include Comparable

  attr_reader :value

  def initialize(value)
    @value = value
  end

  def <=>(other_key)
    value <=> other_key.value
  end
end

k1 = Key.new('foo')
k2 = Key.new('foo')

h = { k1 => 'bar' }
puts h.has_key? k1 # -> true
puts h.has_key? k2 # -> still false ??:·(

As you may expect, the result was the same and I was starting to get really frustrated. I implemented any other method of equality I counld think about. No results. I started hitting walls with my forehead and blaming the flue I currently have. I thought I was missing something obvious.

Once the wall started to crack I remembered having read something about hash keys in the Pragmatic Programmers’ Programming Ruby 1.9 book. I went straight away to look it up and here is what I found:

Hash keys must respond to the message `hash` by returning a hash code, and the hash code for a given key must not change. The keys used in hashes must also be comparable using `eql?`. If `eql?` returns `true` for two keys, then those keys must also have the same hash code. This means that certain classes (such as `Array` and `Hash`) can’t conveniently be used as keys, because their hash values can change based on their contents. Programming Ruby 1.9, Pragmatic Programmers

So the hash and eql? methods were the little mystery and the key to solving the problem. (In Java, you also have the hashCode method besides equals, but I didn’t remember it because Eclipse always generated it for me…)

All primitive types we normally use as hash keys Integer, String and Symbol have a proper implementation of these methods. When you don’t define them they default to the ones in Object which are based on object identity. That’s why h.has_key? k1 returns true.

class Key
  attr_reader :value

  def initialize(value)
    @value = value
  end

  def eql?(other_key)
    value == other_key.value
  end

  def hash
    value.hash
  end
end

k1 = Key.new('foo')
k2 = Key.new('foo')

h = { k1 => 'bar' }
puts h.has_key? k1 # -> true
puts h.has_key? k2 # -> true

I added the methods and the quiz was solved but I’m still embarrassed and still feel like a Ruby beginner.