Forum: Ruby hash of hashes w/defaults doesn't add keys as expected

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
87168b6a1a40c69f8f2ddfc9c42ad71c?d=identicon&s=25 rpardee@gmail.com (Guest)
on 2009-03-11 16:35
(Received via mailing list)
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
E088bb5c80fd3c4fd02c2020cdacbaf0?d=identicon&s=25 Jesús Gabriel y Galán (Guest)
on 2009-03-11 16:55
(Received via mailing list)
On Wed, Mar 11, 2009 at 4:33 PM, rpardee@gmail.com <rpardee@gmail.com>
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.
Ef3aa7f7e577ea8cd620462724ddf73b?d=identicon&s=25 Rob Biedenharn (Guest)
on 2009-03-11 17:15
(Received via mailing list)
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 Biedenharn    http://agileconsultingllc.com
Rob@AgileConsultingLLC.com
87168b6a1a40c69f8f2ddfc9c42ad71c?d=identicon&s=25 Roy Pardee (Guest)
on 2009-03-11 18:55
(Received via mailing list)
Aaaahhh--okay.  I get it now.  Trippy.

Many thanks Jesus & Rob!
This topic is locked and can not be replied to.