Ruby
Rails

Hashのdeep_mergeでHash内部にあるArrayも連結する

Rubyで深いHash同士をマージさせたいことがあります。
例えば、

a = { x: { y: { z1: 'foo' } } }
b = { x: { y: { z2: 'yoo' } } }

という a, b から

c = { x: { y: { z1: 'foo', z2: 'yoo' } } }

という c を得たいとします。

Hash#deep_merge

こういうときに使えるのが Hash#deep_merge です。
これは標準のインスタンスメソッドではなく、ActiveSupportに入っています。
deep_mergeを使えば、以下のように書けます。

c == a.deep_merge(b)

#=> true

Arrayの時は連結したい

さらに以下の要件を追加したいと思います。

d = { x: { y: { z: [1, 2, 3] } } }
e = { x: { y: { z: [4, 5] } } }

f = { x: { y: { z: [1, 2, 3, 4, 5] } } }

de を組み合わせで f を得たいとします。
つまり、deep_mergeしたい2つのHashの中の値として、Arrayが存在する場合、Array同士を連結させたい、ということです。
試しに deep_merge してみます。

d.deep_merge(e)

#=> {:x=>{:y=>{:z=>[4, 5]}}}

deの値で上書きされてしまいました。

Hash#deep_mergeはblockを渡すことができる

ここで、 deep_merge の実装を見てみます。(参考)

def deep_merge(other_hash, &block)
  dup.deep_merge!(other_hash, &block)
end

def deep_merge!(other_hash, &block)
  merge!(other_hash) do |key, this_val, other_val|
    if this_val.is_a?(Hash) && other_val.is_a?(Hash)
      this_val.deep_merge(other_val, &block)
    elsif block_given?
      block.call(key, this_val, other_val)
    else
      other_val
    end
  end
end

deep_mergeが内部的に呼んでいる deep_merge! を見ると、blockを渡すことで、deep_mergeしたい2つのオブジェクトが共にHashでない場合の処理を書けるようです。

deep_mergeでArrayの場合は連結するようにする

以上より「Arrayであれば連結する」という処理をdeep_mergeにblockで渡せばよさそうなことがわかりましたので、そのように実装してみます。

d.deep_merge(e) do |key, this_val, other_val|
  if this_val.is_a?(Array) && other_val.is_a?(Array)
    this_val + other_val
  else
    other_val
  end
end

#=> {:x=>{:y=>{:z=>[1, 2, 3, 4, 5]}}}

ほしい値が得られました。

おわり