ActiveSupportのdeep_mergeに対し、with_indifferent_accessと組み合わせたり、blockを渡してみたりしてみた

Rubyでは Hash#merge を使うことで、2つのハッシュをマージできます。
Hash#merge (Ruby 3.2 リファレンスマニュアル)

 
2つのハッシュでキーが異なる場合は、それぞれのキーを持つハッシュへとマージされます。

h1 = { name: 'foo' }
h2 = { color: 'red' }

h1.merge(h2)
#=> { name: 'foo', color: 'red' }

 
一方、2つのハッシュでキーが重複する場合は、 merge メソッドに渡した引数のハッシュの値が使われます。

h1 = { name: 'foo' }
h2 = { name: 'bar' }

h1.merge(h2)
#=> { name: 'bar' }

 
そのため、ネストしたハッシュをマージしようとしても、ネストしたハッシュが統合されず、 merge メソッドに渡した引数のハッシュが使われます。

h1 = { name: {
  apple: 'シナノゴールド',
} }
h2 = { name: {
  sweet_potato: 'シルクスイート'
} }

h1.merge(h2)
#=> { name: { sweet_potato: 'シルクスイート' } }

 
ネストしたハッシュもうまいことマージする方法がないかを調べたところ、 ActiveSupport#deep_merge がありました。

 
そこで、使ってみたときのことをメモとして残します。

 
目次

 

環境

 

deep_mergeによる、ネストしたハッシュのマージ

同じキーがない場合、そのままマージ

Hash#merge と同じ挙動が、ネストしたハッシュにも適用されています。

h1 = { name: {
  apple: 'シナノゴールド',
} }
h2 = { name: {
  sweet_potato: 'シルクスイート'
} }

h1.deep_merge(h2)
#=> { name: { apple: 'シナノゴールド', sweet_potato: 'シルクスイート' } }

 

同じキーがある場合、deep_mergeの引数を採用する

こちらも、Hash#merge と同じ挙動が、ネストしたハッシュにも適用されています。

h1 = { name: {
  apple: 'シナノゴールド',
} }
h2 = { name: {
  apple: '秋映',
} }

h1.deep_merge(h2)
#=> { name: { apple: '秋映' } }

 

シンボルと文字列で同じ名前をもつハッシュのマージ

deep_mergeだけだと、別のキー扱い

Hash#merge ではシンボルと文字列は別のキーという扱いでマージされます。

h1 = { name: 'foo' }
h2 = { 'name' => 'bar' }

h1.merge(h2)
#=> { name: 'foo', 'name' => 'bar' }

 
ActiveSupport#deep_merge も同様な挙動です。

h1 = { name: 'foo' }
h2 = { 'name' => 'bar' }

h1.deep_merge(h2)
#=> { name: 'foo', 'name' => 'bar' }

 

deep_merge + with_indifferent_access を組み合わせてマージ

ActiveSupportには、ハッシュのシンボルと文字列を同様に扱う with_indifferent_access メソッドがあります。
11.8 ハッシュキーのシンボルと文字列を同様に扱う(indifferent access) | Active Support コア拡張機能 - Railsガイド

 
そこで、 deep_mergewith_indifferent_access を組み合わせて使うと、同じキーとしてマージされました。

h1 = { name: 'foo' }
h2 = { 'name' => 'bar' }

h1.with_indifferent_access.deep_merge(h2)
#=> #<HashWithIndifferentAccess { 'name' => 'bar' }>

 
ネストしているハッシュでも、同じキーという扱いでマージされました。

h1 = { name: {
  apple: 'シナノゴールド',
} }
h2 = { name: {
  'apple' => '秋映',
} }

h1.with_indifferent_access.deep_merge(h2)
#=> #<HashWithIndifferentAccess { "name" => #<HashWithIndifferentAccess { "apple" => "秋映" }> }>

 

deep_mergeにblockを渡す

重複キーに対する値を決められる

ActiveSupport#deep_merge の説明を見ると、

Like with Hash#merge in the standard library, a block can be provided to merge values

https://api.rubyonrails.org/v7.0.4/classes/Hash.html#method-i-deep_merge

とのことでした。

次に Hash#merge の説明を見ると

ブロック付きのときはブロックを呼び出してその返す値を重複キーに対応する値にします。

https://docs.ruby-lang.org/ja/latest/method/Hash/i/merge.html

とのことです。

 
試してみると、たしかに Hash#merge と同じ挙動でした。

# フラットなハッシュ
h1 = { name: 'foo' }
h2 = { name: 'bar' }

h1.deep_merge(h2) do |_key, this_val, other_val|
  this_val + other_val
end
#=> { name: 'foobar' }


# ネストしたハッシュ
h1 = { name: {
  apple: 'シナノゴールド',
} }
h2 = { name: {
  apple: '秋映',
} }

h1.deep_merge(h2) do |key, this_val, other_val|
  "key: #{key} > #{this_val} and #{other_val}"
end
#=> { name: { apple: 'key: apple > シナノゴールド and 秋映' }}

 
以上より、 merge 同様、 deep_merge もblockにてマージ結果を指定できることから、

  • 結果のハッシュにキーがなければ、その値を入れる
  • 結果のハッシュにキーがあれば、加算する

ような場合でも、 if ではなく deep_merge を使って表現できそうです。

# if - else を使う
result = {}
[{ apple: 100 }, { sweet_potato: 50}, { apple: 200 }].each do |item|
  item.each do |k, v|
    if result.has_key?(k)
      result[k] += v
    else
      result[k] = v
    end
  end
end


# deep_mergeを使う
result = {}

[{ apple: 100 }, { sweet_potato: 50}, { apple: 200 }].each do |item|
  result.deep_merge(item) { |_key, this_val, other_val| this_val + other_val }
end

 
ちなみに、もし、シンボルと文字列で同じ名前なキーを持つ場合でも加算したい場合は、 with_indifferent_access を合わせて使います。

h1 = { amount: {
  apple: 100,
  sweet_potato: 50
} }
h2 = { amount: {
  'apple' => 200,
} }

h1.with_indifferent_access.deep_merge(h2) do |_key, this_val, other_val|
  this_val + other_val
end
#=> #<HashWithIndifferentAccess { "amount" => #<HashWithIndifferentAccess { "apple" => 300, "sweet_potato" => 50 }> }>

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app

プルリクはこちらです。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/11

なお、記事中のソースコードはhelperとhelper_specに記載してあります。