Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Ruby: Storing a Hash in a Hash with default value Hash, for non-existent key

Why does attempting to store nested values for Hash.new({}) work the way it does? What’s actually happening under the hood? What would its use-case be?

When creating a Hash that has a default value of {}

a = Hash.new({})
=> {}
a.default
=> {}

querying a non-existent key will return an empty hash as expected

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

a[:foo]
=> {}

however when attempting to assign a value to a key that does not yet exist

a[:foo][:bar] = 'baz'
=> "baz"

the hash still appears to be empty

a
=> {}

however fetching the parent key will return the nested hash.

a[:foo]
=> {:bar=>"baz"}

Even more confusingly, this new hash has now become the parent hash’s default value

a.default
=> {:bar=>"baz"}

such that querying a non-existent key will return that value

a[:biz]
=> {:bar=>"baz"}

This can be solved by doing

a[:foo] = {} unless a.key? :foo
a[:foo][:bar] = 'baz'
a
=> {:foo=>{:bar=>"baz"}}

Other similar questions also suggest a = Hash.new { |h,k| h[k] = Hash.new(&h.default_proc) } which works for storing new keys, but also creates empty hashes for fetch operations e.g.

a[:baz] == 2
a
=> {:baz=>{}}

Is there some way, other than writing a method, to get the hash to create nested hashes if necessary when storing values, but not when fetching values?

>Solution :

Remember that in Ruby1 you’re storing a default object reference, not a default that’s cloned. Even though it isn’t your intent, you’re asking for the default for any missing key to be the same object. For simple values like Hash.new(0) the default isn’t altered when a new value is assigned, it’s replaced, but with a nested hash you’re explicitly altering the value.

What you want is this expressed more minimally as:

a = Hash.new { |h,k| h[k] = { } }

If you’re ever confused by why things end up blending like this, check with object_id which tells you the "identity" of a given object.

Consider:

a = Hash.new({ })
a[0].object_id == a[1].object_id
# => true

b = Hash.new { |h,k| h[k] = { } }
b[0].object_id == b[1].object_id
# => false

Where here you can see each "slot" is independent.

One downside to these auto-instantiating models is, as you point out, it will create entries you may not necessarily want. To avoid that you’ll need to tread more carefully, as in:

if a.key?(:baz) && a[:baz] == 2
  # ...
end

1 JavaScript, Python and others also exhibit this behaviour, at least for non-primitive types.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading