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