困ったこと
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}
以上。
参考