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
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?
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.
If you don't want to create empty hashes for fetch operations, you can use #freeze to protect the default value from accidental modification.
a = Hash.new({}.freeze)
a[:foo] #=> {}
a #=> {} still empty, no :foo key created
a[:foo][:bar] = 'baz' #FrozenError because you're trying to modify the default value
a[:foo] = a[:foo].merge(:bar => 'baz')
a #=> {:foo=>{:bar=>"baz"}}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With