1

I'd like to create a new Hash with nested default values. I thought it should be like

h = Hash.new(count: 0, rating: 0)

So I can do stuff like

h['a'][:count] += 1
h['a'][:rating] += 1

and so on. But when I try it in the console it looks like this:

2.3.1 :046 > h = Hash.new(count: 0, rating: 0)
 => {} 
2.3.1 :047 > h["a"]
 => {:count=>0, :rating=>0} 
2.3.1 :048 > h["a"][:count]
 => 0 
2.3.1 :049 > h["a"][:count] += 1
 => 1 
2.3.1 :050 > h["b"][:count] += 1
 => 2
2.3.1 :051 > h
 => {}

So my questions are:

  • Why is h["b"][:count] += 1 returning 2 and not 1?
  • why is h empty?

Thanks in advance!

Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
kernification
  • 511
  • 1
  • 5
  • 15

2 Answers2

4

The doc for Hash::new explains the three ways of initializing a hash and, in your case, you are using an object in the Hash constructor:

If obj is specified, this single object will be used for all default values.

If you want that each missing key creates it's own object, create the hash with a block, like this:

h = Hash.new { |h,k| h[k] = { count: 0, rating: 0 } }

Then:

2.6.3 :012 > h
 => {} 
2.6.3 :013 > h['a'][:count] = 5
 => 5 
2.6.3 :015 > h
 => {"a"=>{:count=>5, :rating=>0}} 
luis.parravicini
  • 1,214
  • 11
  • 19
2

You can find this behaviour documented in the Hash::new documentation:

new → new_hash

new(obj) → new_hash

new {|hash, key| block } → new_hash

Returns a new, empty hash. If this hash is subsequently accessed by a key that doesn't correspond to a hash entry, the value returned depends on the style of new used to create the hash. In the first form, the access returns nil. If obj is specified, this single object will be used for all default values. If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block's responsibility to store the value in the hash if required.

h = Hash.new("Go Fish")
h["a"] = 100
h["b"] = 200
h["a"]           #=> 100
h["c"]           #=> "Go Fish"
# The following alters the single default object
h["c"].upcase!   #=> "GO FISH"
h["d"]           #=> "GO FISH"
h.keys           #=> ["a", "b"]

# While this creates a new default object each time
h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }
h["c"]           #=> "Go Fish: c"
h["c"].upcase!   #=> "GO FISH: C"
h["d"]           #=> "Go Fish: d"
h.keys           #=> ["c", "d"]

In your example h["a"] and h["b"] both return the exact same default hash object. Meaning that if you change h["a"], h["b"] is also changed. Since you never set h["a"] or h["b"] the hash appears empty.

To assign a new hash on access you'll have to use the block syntax as shown by luis.parravicini.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52