2

I'm running ruby 2.2.2:

$ ruby -v
ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]

Here I am initializing a hash with one key :b that has a value of Hash.new({})

irb(main):001:0> a = { b: Hash.new({}) }
=> {:b=>{}}

Now, I'm going to attempt to auto-vivify another hash at a[:b][:c] with a key 'foo' and a value 'bar'

irb(main):002:0> a[:b][:c]['foo'] = 'bar'
=> "bar"

At this point, I expected that a would contain something like:

{ :b => { :c => { 'foo' => 'bar' } } }

However, that is not what I'm seeing:

irb(main):003:0> a
=> {:b=>{}}
irb(main):004:0> a[:b]
=> {}
irb(main):005:0> a[:b][:c]
=> {"foo"=>"bar"}

This differs from the following:

irb(main):048:0> a = { :b => { :c => { "foo" => "bar" } } }
=> {:b=>{:c=>{"foo"=>"bar"}}}
irb(main):049:0> a
=> {:b=>{:c=>{"foo"=>"bar"}}}

So what is going on here?

I suspect this is something to do with Hash.new({}) returning a default value of {}, but I'm not exactly sure how to explain the end result...

l8nite
  • 5,042
  • 1
  • 20
  • 23
  • 1
    Please read the accepted answer here: http://stackoverflow.com/questions/5878529/how-to-assign-hashab-c-if-hasha-doesnt-exist It has a pretty good explanation about the behavior. In your example, try to print `a[:b][:x]` – Aguardientico Jul 25 '15 at 04:50
  • Ah, didn't notice this comment until I was done drafting my answer. You are right, this nails it too. Thanks! – l8nite Jul 25 '15 at 05:20
  • My great aunt once told me autovivifaction was a Perl thing. I'm not familiar with the term in the Ruby context. – Cary Swoveland Jul 25 '15 at 07:28
  • @CarySwoveland caught me! :) – l8nite Jul 25 '15 at 17:18

2 Answers2

1

Apologies for answering my own question, but I figured out what is happening.

The answer here is that we are assigning into the default hash being returned by a[:b], NOT a[:b] directly.

As before, we're going to create a hash with a single key of b and a value of Hash.new({})

irb(main):068:0> a = { b: Hash.new({}) }
=> {:b=>{}}

As you might expect, this should make things like a[:b][:unknown_key] return an empty hash {}, like so:

irb(main):070:0> a[:b].default
=> {}
irb(main):071:0> a[:b][:unknown_key]
=> {}
irb(main):072:0> a[:b].object_id
=> 70127981905400
irb(main):073:0> a[:b].default.object_id
=> 70127981905420

Notice that the object_id for a[:b] is ...5400 while the object_id for a[:b].default is ...5420

So what happens when we do the assignment from the original question?

a[:b][:c]["foo"] = "bar"

First, a[:b][:c] is resolved:

irb(main):075:0> a[:b][:c].object_id
=> 70127981905420

That's the same object_id as the .default object, because :c is treated the same as :unknown_key from above!

Then, we assign a new key 'foo' with a value 'bar' into that hash.

Indeed, check it out, we've effectively altered the default instead of a[:b]:

irb(main):081:0> a[:b].default
=> {"foo"=>"bar"}

Oops!

l8nite
  • 5,042
  • 1
  • 20
  • 23
0

The answer is probably not as esoteric as it might seem at the onset, but this is just the way Ruby is handling that Hash. If your initial Hash is the:

a = { b: Hash.new({}) } 
b[:b][:c]['foo'] = 'bar'

Then seeing that each 'layer' of the Hash is just referencing the next element, such that:

a                # {:b=>{}}
a[:b]            # {}
a[:b][:c]        # {"foo"=>"bar"}
a[:b][:c]["foo"] # "bar"

Your idea of:

{ :b => { :c => { 'foo' => 'bar' } } }

Is somewhat already accurate, so it makes me think that you already understand what's happening, but felt unsure of what was happening due to the way IRB was perhaps displaying it.

If I'm missing some element of your question though, feel free to comment and I'll revise my answer. But I feel like you understand Hashes better than you're giving yourself credit for in this case.

Tony DiNitto
  • 1,244
  • 1
  • 16
  • 28
  • Thanks! Any idea why `a[:b]` doesn't display `{:c=>{"foo"=>"bar"}}`? or `a[:b].keys` is an empty array `[]` ? If I initialize my expected hash directly, I see those values. – l8nite Jul 25 '15 at 04:30
  • Ah. Ruby doesn't show children of Hashes 'easily'. I don't want to be a "possible duplicate" person here, but there's a similar question regarding this 'weirdness' as you've pointed out here -> http://stackoverflow.com/questions/1820451/ruby-style-how-to-check-whether-a-nested-hash-element-exists . Your question is a good question, but it's just kind of the way nested Hashes behave in Ruby. – Tony DiNitto Jul 25 '15 at 04:34
  • I read that question before posting, and I don't think it's asking the same thing. I've updated my post to show the difference in behavior when assigning the expected hash directly. In that case, I see every key, so I am not convinced this is simply an artifact of `irb`. – l8nite Jul 25 '15 at 04:40
  • I agree - it's not an artifact of irb, as well as the idea that the other question was different. I only mention the other question as it sort of shows a bit about children Hashes not showing up like when you have your Hash declared with the `({})` default. When Ruby sees you haven't declared something explicitly, it a) prevents further declaration errors from having issues with 'nil', but b) simply doesn't show those children - even if the children are subsequently declared (eg like `a = [:b][:c]` having an actual declaration, vs no declaration ever for `a[:b]` explicitly) – Tony DiNitto Jul 25 '15 at 04:49
  • 1
    I figured it out and posted an answer to my own question. Thank you for the replies, they definitely got me moving in the right direction. – l8nite Jul 25 '15 at 05:13