l8nite
l8nite

Reputation: 5182

Ruby hash vivification weirdness

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...

Upvotes: 2

Views: 91

Answers (2)

l8nite
l8nite

Reputation: 5182

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!

Upvotes: 1

Tony DiNitto
Tony DiNitto

Reputation: 1239

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.

Upvotes: 0

Related Questions