I'm trying to initialize a Hash in ruby, by using another hash with default values. I want a deep copy but I only ever seem to get a shallow copy.
Here is an example:
DEFAULT_HASH = { a: 0, b: 1 }.freeze
my_hash = DEFAULT_HASH.dup
my_hash[:a] = 4 
Now the value of a in "my_hash" and in DEFAULT_HASH is 4. I only want the value in my hash to change.
I have tried other approaches too:
my_hash = {}.merge DEFAULT_HASH
and
my_hash.merge! DEFAULT_HASH
All of these produce the same effect. What is the best way to achieve this sort of initialization. I'm also working with nested hashes which adds to the complexity a bit.
i.e. my DEFAULT_HASH looks like:
DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
Would this affect how to do this?
EDIT: Nested Hash case
DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
a=DEFAULT_HASH.dup
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
a[:b][:a]=12
=> 12 
DEFAULT_HASH
=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>12, :b=>1}} 
To @pjs's point, Hash#dup will 'do the right thing' for the top level of a hash. For nested hashes however, it still fails.
If you're open to using a gem, consider using deep_enumerable, a gem I wrote for exactly this purpose (among others).
DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
dupped = DEFAULT_HASH.dup
dupped[:a][:a] = 'updated'
puts "dupped:       #{dupped.inspect}"
puts "DEFAULT_HASH: #{DEFAULT_HASH.inspect}"
require 'deep_enumerable'
DEFAULT_HASH = { a:{a:1, b:2}, b:{a:2, b:1} }
deep_dupped = DEFAULT_HASH.deep_dup
deep_dupped[:a][:a] = 'updated'
puts "deep_dupped:  #{deep_dupped.inspect}"
puts "DEFAULT_HASH: #{DEFAULT_HASH.inspect}"
Output:
dupped:       {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}
DEFAULT_HASH: {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}
deep_dupped:  {:a=>{:a=>"updated", :b=>2}, :b=>{:a=>2, :b=>1}}
DEFAULT_HASH: {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}}
Alternatively, you could try something along the lines of:
def deep_dup(h)
  Hash[h.map{|k, v| [k,
    if v.is_a?(Hash)
      deep_dup(v)
    else
      v.dup rescue v
    end
  ]}]
end
Note, this last function is nowhere near as well tested as deep_enumerable.
You can easily create your own deep dup method, using Marshal::dump and Marshal::load:
def deep_dup(obj)
  Marshal.load(Marshal.dump(obj))
end
obj can be most any Ruby object (e.g., nested mix of arrays and hashes).
h = { a: { b: { c: { d: 4 } } } }
g = deep_dup(h)         #=> {:a=>{:b=>{:c=>{:d=>4}}}}
g[:a][:b][:c][:d] = 44  #=> 44
g                       #=> {:a=>{:b=>{:c=>{:d=>44}}}} 
h                       #=> {:a=>{:b=>{:c=>{:d=>4}}}} 
For your example:
DEFAULT_HASH = { a: { a:1, b:2 }, b: { a:2, b:1 } }
  #=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
h = deep_dup(DEFAULT_HASH)
  #=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
h[:b][:a] = 12
  #=> 12
h #=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>12, :b=>1}}
DEFAULT_HASH
  #=> {:a=>{:a=>1, :b=>2}, :b=>{:a=>2, :b=>1}} 
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