Brett
Brett

Reputation: 6030

Extending a ruby class (hash) with new function (recursive_merge)

If I want to recursively merge 2 hashes, I can do so with the following function:

def recursive_merge(a,b)
  a.merge(b) {|key,a_item,b_item| recursive_merge(a_item,b_item) }
end

This works great, in that I can now do:

aHash = recursive_merge(aHash,newHash)

But I'd like to add this as a self-updating style method similar to merge!. I can add in the returning function:

class Hash
  def recursive_merge(newHash)
    self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
  end
end

But am not sure how to re-create the bang function that updates the original object without association.

class Hash
  def recursive_merge!(newHash)
    self.merge { |key,a_item,b_item| a_item.recursive_merge(b_item) }
    # How do I set "self" to this new hash?
  end
end

edit example as per comments.

h={:a=>{:b => "1"}
h.recursive_merge!({:a=>{:c=>"2"})
 => {:a=>{:b=>"1", :c="2"}}

The regular merge results in :b=>"1" being overwritten by :c="2"

Upvotes: 2

Views: 2437

Answers (1)

Daniel Stevens
Daniel Stevens

Reputation: 772

Use merge! rather than attempt to update self. I don't believe it makes sense to use merge! anywhere but at the top level, so I wouldn't call the bang version recursively. Instead, use merge! at the top level, and call the non-bang method recursively.

It may also be wise to check both values being merged are indeed hashes, otherwise you may get an exception if you attempt to recursive_merge on a non-hash object.

#!/usr/bin/env ruby

class Hash
  def recursive_merge(other)
    self.merge(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
  end

  def recursive_merge!(other)
    self.merge!(other) { |key, value1, value2| value1.is_a?(Hash) && value2.is_a?(Hash) ? value1.recursive_merge(value2) : value2}
  end
end


h1 = { a: { b:1, c:2 }, d:1 }
h2 = { a: { b:2, d:4 }, d:2 }
h3 = { d: { b:1, c:2 } }


p h1.recursive_merge(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1.recursive_merge(h3) #  => {:a=>{:b=>1, :c=>2}, :d=>{:b=>1, :c=>2}}

p h1.recursive_merge!(h2) # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}
p h1 # => {:a=>{:b=>2, :c=>2, :d=>4}, :d=>2}

If you have a specific reason to fully merge in place, possibly for speed, you can experiment with making the second function call itself recursively, rather than delegate the recursion to the first function. Be aware that may produce unintended side effects if the hashes store shared objects.

Example:

h1 = { a:1, b:2 }
h2 = { a:5, c:9 }
h3 = { a:h1, b:h2 }
h4 = { a:h2, c:h1 }

p h3.recursive_merge!(h4)
# Making recursive calls to recursive_merge
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>1, :b=>2}}
# Making recursive calls to recursive_merge!
# => {:a=>{:a=>5, :b=>2, :c=>9}, :b=>{:a=>5, :c=>9}, :c=>{:a=>5, :b=>2, :c=>9}}

As you can see, the second (shared) copy of h1 stored under the key :c is updated to reflect the merge of h1 and h2 under the key :a. This may be surprising and unwanted. Hence why I recommend using recursive_merge for the recursion, and not recursive_merge!.

Upvotes: 1

Related Questions