LoginSignup
3
0

More than 1 year has passed since last update.

[Ruby] 階層の深いHashのmerge(deep_merge)と、そのコード簡略化

Last updated at Posted at 2021-06-08

困ったこと

Hash{}のmergeメソッドが、同一階層内の別keyデータを保持してくれない。

irb(main):001:0> h = {a:{aa:11, ab:11}}
irb(main):002:0> h.merge({a:{aa:99}})
    ==>{:a=>{:aa=>99}}

==> {:a=>{:aa=>99, :ab=>11}} になってほしい

TL;DR

↓ソースをコピペすればOK

class Hash
  def deep_merge(*others)
    self.clone.deep_merge!(*others)
  end

  def deep_merge!(*others)
    _child_merge = -> (_, self_val, other_val){
      if self_val.instance_of?(Hash) && other_val.instance_of?(Hash)
        self_val.merge other_val, &_child_merge
      else
        other_val
      end
    }
    self.merge! *others, &_child_merge
  end
end

↓使用例

$ irb
irb(main):001:1* class Hash
irb(main):002:2*   def deep_merge(*others)
irb(main):003:2*     self.clone.deep_merge!(*others)
irb(main):004:1*   end
irb(main):005:1*   
irb(main):006:2*   def deep_merge!(*others)
irb(main):007:2*     _child_merge = -> (_, self_val, other_val){
irb(main):008:3*     if self_val.instance_of?(Hash) && other_val.instance_of?(Hash)
irb(main):009:3*       self_val.merge other_val, &_child_merge
irb(main):010:3*     else
irb(main):011:3*       other_val
irb(main):012:2*     end
irb(main):013:1*   }
irb(main):014:1*   self.merge! *others, &_child_merge
irb(main):015:0* end
irb(main):016:-> end
    ==>:deep_merge!
irb(main):017:0> h = {a:{aa:11, ab:11}}
irb(main):018:0> h.deep_merge({a:{aa:99}}) #deep_mergeは元データを変更しない
    ==>{:a=>{:aa=>99, :ab=>11}}
irb(main):019:0> h
    ==>{:a=>{:aa=>11, :ab=>11}}
irb(main):020:0> h.deep_merge!({a:{aa:99}}) #deep_merge!は元データも変更する
    ==>{:a=>{:aa=>99, :ab=>11}}
irb(main):021:0> h
    ==>{:a=>{:aa=>99, :ab=>11}}

説明

どうやってソースを組み立てたかと、コード簡略化について説明していきます。

困ったことの解消

まずHashのmergeメソッドについて見てみます。

このメソッドは、同一階層内でなければ、複数の値を保持してくれます。

irb(main):001:0> h1 = { "a" => 100, "b" => 200 }
irb(main):002:0> h2 = { "c" => 300, "d" => 400 }
irb(main):003:0> h1.merge(h2)
    ==>{"a"=>100, "b"=>200, "c"=>300, "d"=>400}

また以下のように、
keyが重複した場合のみ、merge元データとmerge先データでイテレータ処理ができます。

irb(main):031:0> h1 = { "a" => 100, "b" => 200 }
irb(main):032:0> h2 = { "b" => 246, "c" => 300 }
irb(main):033:0> h1.merge(h2) {|key, oldval, newval| newval - oldval}
    ==>{"a"=>100, "b"=>46,  "c"=>300}

そして、イテレータのなかでは1階層落ちたデータとなります。

irb(main):034:0> h1 = { "a" => 100, "b" => {"bb"=>210, "bc"=>220} }
irb(main):035:0> h2 = { "b" => {"bd"=>230}, "c" => 300 }
irb(main):036:1* h1.merge(h2) {|key, oldval, newval|
irb(main):037:1*   p oldval
irb(main):038:1*   p newval
irb(main):039:0> }
{"bb"=>210, "bc"=>220}
{"bd"=>230}

つまり、「keyが重複した場合」にイテレータ内で「再びmerge」すれば、
階層が深くなっても値を落とさずに処理できます。

irb(main):040:0> h1 = { "a" => 100, "b" => {"bb"=>210, "bc"=>220} }
irb(main):041:0> h2 = { "b" => {"bd"=>230}, "c" => 300 }
irb(main):042:0> h1.merge(h2) {|key, oldval, newval| oldval.merge(newval)}
    ==>{"a"=>100, "b"=>{"bb"=>210, "bc"=>220, "bd"=>230}, "c"=>300} # データが保持されてる!

やりたかった処理(=同一階層内の別keyデータを保持)が
実現できるメドが立ちました。

再帰処理の追加

ただ1階層ではダメで、階層を深堀りしていく必要があります。

そこで、処理をメソッド化し、
メソッド内部で、再帰的にイテレータ処理を行うようにします。

また、再帰的に処理を行うなかで、片方がHashでない場合は、
merge先データ(=newval)を正として上書きするので、イテレータ処理は不要となります。

これらを反映するとこうなります。

irb(main):043:1* def child_merge(key, oldval, newval)
irb(main):044:2*   if oldval.instance_of?(Hash) && newval.instance_of?(Hash)
irb(main):045:2*     return oldval.merge(newval) {|key, oldval, newval| child_merge(key, oldval, newval)}
irb(main):046:2*   else
irb(main):047:2*     return newval
irb(main):048:1*   end
irb(main):049:0> end
    ==>:child_merge
irb(main):050:0> h1 = { "a" => 100, "b" => {"bb"=>{"bbb"=>210}, "bc"=>220} }
irb(main):051:0> h2 = { "b" => {"bb"=>{"bbe"=>240}, "bd"=>230}, "c" => 300 }
irb(main):052:0> h1.merge(h2) {|key, oldval, newval| child_merge(key, oldval, newval)}
    ==>{"a"=>100, "b"=>{"bb"=>{"bbb"=>210, "bbe"=>240}, "bc"=>220, "bd"=>230}, "c"=>300}

リファクタ

ここですこし、リファクタをします。

  • 簡易な処理なのでreturnを省略
def child_merge(key, oldval, newval)
  if oldval.instance_of?(Hash) && newval.instance_of?(Hash)
    oldval.merge(newval) {|key, oldval, newval| child_merge(key, oldval, newval)}
  else
    other_val
  end
end
  • 使わないキーは_にして省略
def child_merge(_, oldval, newval)
  • この変数3つ|key, oldval, newval|が長いので、lambda関数に変更
    (意外とこの、"引数"+"ブロック処理"のlambda記法が見つからなくて、めちゃくちゃ探しました)
child_merge = -> (_, oldval, newval){
  if oldval.instance_of?(Hash) && newval.instance_of?(Hash)
    oldval.merge newval, &child_merge
  else
    newval
  end
}

けっこうシンプルになりました。

Hashクラスへの適用

最後に、(Rubyの大きな特徴のひとつですが)classにメソッドを直接overrideできるので、
そちらを利用して、よりコードをシンプルにしていきます。

メソッドの変数名も、Hash#mergeに合わせて self_val, other_valにします。

class Hash
  def deep_merge(other_h)
    _child_merge = -> (_, self_val, other_val){
      if self_val.instance_of?(Hash) && other_val.instance_of?(Hash)
        self_val.merge other_val, &_child_merge
      else
        other_val
      end
    }
    self.merge other_h, &_child_merge
  end
end

(private変数は_xxxにしてます。ruby的には意味ないです。ただの好みです)

また、Hash#mergeは複数ハッシュも対応しているので、その対応も追加します。

class Hash
  def deep_merge(*others)
    (省略)
    self.merge *others, &_child_merge
  end
end

元データも変更する、Hash#merge!にも対応します。

class Hash
  def deep_merge!(*others)
    (省略)
    self.merge! *others, &_child_merge
  end
end

そして最後に、通常のdeep_mergeも追加して、完成です。

class Hash
  def deep_merge(*others)
    self.clone.deep_merge!(*others)
  end

  def deep_merge!(*others)
    _child_merge = -> (_, self_val, other_val){
      if self_val.instance_of?(Hash) && other_val.instance_of?(Hash)
        self_val.merge other_val, &_child_merge
      else
        other_val
      end
    }
    self.merge! *others, &_child_merge
  end
end

使用方法

TL;DRの使用例にもあるように、
Hashクラスの変数であれば、メソッドを直接呼び出せるようになります。

irb(main):001:1* class Hash
(省略)
irb(main):016:-> end
    ==>:deep_merge!
irb(main):017:0> h = {a:{aa:11, ab:11}}
irb(main):018:0> h.deep_merge({a:{aa:99}})
    ==>{:a=>{:aa=>99, :ab=>11}}
irb(main):019:0> h
    ==>{:a=>{:aa=>11, :ab=>11}}
irb(main):020:0> h.deep_merge!({a:{aa:99}})
    ==>{:a=>{:aa=>99, :ab=>11}}
irb(main):021:0> h
    ==>{:a=>{:aa=>99, :ab=>11}}
irb(main):022:0> h.deep_merge({a:{ac:11}}, {b:1})
    ==>{:a=>{:aa=>99, :ab=>11, :ac=>11}, :b=>1}
irb(main):023:0> h.deep_merge({a:{ac:11}}, {b:1}, {b:9})
    ==>{:a=>{:aa=>99, :ab=>11, :ac=>11}, :b=>9}

以上。

参考

3
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0