Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Duplicating a Hash in Ruby

Tags:

ruby

hash

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}} 
like image 936
System123 Avatar asked Oct 27 '25 13:10

System123


2 Answers

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.

like image 144
user12341234 Avatar answered Oct 30 '25 05:10

user12341234


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}} 
like image 31
Cary Swoveland Avatar answered Oct 30 '25 06:10

Cary Swoveland



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!