TIL: Hash Edition

I was reviewing some code for a co-worker today and learned something new about Hash. My co-worker was using a ruby hash not as a dictionary but as a O(1) look up collection (also known as a hash table). Because of this there were a few places in his code that looked something like this:

1
2
3
4
5
6
7
h = {}

def add_the_keys(keys_to_add)
  keys_to_add.each do |k|
    h[k] = 0
  end
end

Whenever I see the same value being used for all the keys in a hash I think “use a hash with a default value”. I dutiful rewrote the code to look this like:

1
2
3
4
5
6
7
h = Hash.new(0)

def add_the_keys(keys_to_add)
  keys_to_add.each do |k|
    h[k]
  end
end

And then the tests started failing. Out of curiousity I tried this instead:

1
2
3
4
5
6
7
h = Hash.new { |h, k| h[k] = 0 }

def add_the_keys(keys_to_add)
  keys_to_add.each do |k|
    h[k]
  end
end

And the tests passed again. What the heck? So I went into irb and tried a couple things: kxx ~~~ ruby h1 = {}

h1[:a] = 0 # 0 h2[:b] # nil

h1.keys # [:a]

h2 = Hash.new(0)

h2[:a] = 0 # 0 h2[:b] # 0

h2.keys # [:a]

h3 = Hash.new { h, k h[k] = 0 }

h3[:a] = 0 # 0 h3[:b] # 0

h3.keys # [:a, :b] ~~~

Moral of the story: Keys are not added to a hash unless there is an assigment or there is a default_proc. Also, if you want a collection with O(1 ) key access use Set. It is a hash under the hood but using Set reveals your intent.

P.S. For extra fun try this:

1
2
3
4
h4 = Hash.new { |h, k| h[k] }

h4[:a] = 0    # 0
h4[:b]        # hang