Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

ruby

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?

like image 520
francesco Avatar asked Dec 28 '25 06:12

francesco


2 Answers

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.

like image 141
tadman Avatar answered Dec 30 '25 23:12

tadman


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"}}
like image 27
Daniel Avatar answered Dec 30 '25 22:12

Daniel