Hash of hashes w/defaults doesn't add keys as expected


#1

Hey All,

This surprised me:

hash_hash = Hash.new(Hash.new(0))

hash_hash[:foo][:bar] += 1
hash_hash[:foo][:baz] += 1

puts(hash_hash[:foo].keys.inspect) # => [:baz, :bar]
puts(hash_hash.keys.inspect) # => [] ?

Shouldn’t hash_hash.keys == [:foo]? I’m running ruby 1.8.6
(2007-03-13 patchlevel 0) [i386-mswin32].

If this is expected behavior, what’s the easiest/best performing way
to make sure those keys are added?

Many thanks!

-Roy


#2

On Wed, Mar 11, 2009 at 4:33 PM, removed_email_address@domain.invalid removed_email_address@domain.invalid
wrote:

puts(hash_hash.keys.inspect) # => [] ?

Shouldn’t hash_hash.keys == [:foo]? I’m running ruby 1.8.6
(2007-03-13 patchlevel 0) [i386-mswin32].

The idiom you are using to construct the hash specifies what the hash
returns when accesing a non-existing key.
It doesn’t set that value for that key in the hash:

irb(main):001:0> a = Hash.new(0)
=> {}
irb(main):002:0> a[2]
=> 0
irb(main):003:0> a
=> {}

You example:

hash_hash[:foo][:bar] += 1

Could be split in several parts, so that you better understand what’s
going on:

hash_hash[:foo] => this returns the default object (which is a hash),
but doesn’t create an entry in the hash. For simplicity in the
example, let’s call this object default_object. Your code is
equivalent then to:

default_object[:bar] = default_object[:bar] + 1

(because of the +=). The right hand side default_object[:bar] returns
0, which is the default object of the default_object. Plus 1 makes 1,
and this is assigned to the key :bar in the default object. That’s why
you see :bar => 1 when you access a non-existing key in the hash_hash,
because you have modified the default object. The hash_hash is not
modified with any extra entry, though.

If this is expected behavior, what’s the easiest/best performing way
to make sure those keys are added?

You want the block form of Hash.new:

irb(main):009:0> hash_hash = Hash.new {|h,k| h[k] = Hash.new(0)}
=> {}
irb(main):010:0> hash_hash[:foo][:bar] += 1
=> 1
irb(main):011:0> hash_hash
=> {:foo=>{:bar=>1}}

If you want an infinitely nested hash, this is a neat trick:

irb(main):012:0> nested_hash = Hash.new {|h,k| h[k] = Hash.new
&h.default_proc}
=> {}
irb(main):013:0> nested_hash[:a][:b][:c][:d] = 1
=> 1
irb(main):014:0> nested_hash
=> {:a=>{:b=>{:c=>{:d=>1}}}}

Hope this helps,

Jesus.


#3

On Mar 11, 2009, at 11:52 AM, Jesús Gabriel y Galán wrote:

puts(hash_hash[:foo].keys.inspect) # => [:baz, :bar]

Jesus’s answer made me realize the true behavior (I was scratching my
head on this one a bit).

Perhaps this will cement it:

hash_hash[:notfoo].keys

It gets the same default object for the missing :notfoo key as it got
for the missing :foo.

-Rob

(because of the +=). The right hand side default_object[:bar] returns

&h.default_proc}
=> {}
irb(main):013:0> nested_hash[:a][:b][:c][:d] = 1
=> 1
irb(main):014:0> nested_hash
=> {:a=>{:b=>{:c=>{:d=>1}}}}

Hope this helps,

Jesus.

Rob B. http://agileconsultingllc.com
removed_email_address@domain.invalid


#4

Aaaahhh–okay. I get it now. Trippy.

Many thanks Jesus & Rob!