ElixirのMapをdeep mergeする方法を調べたのでメモ。
はじめに
たまたまあるElixirのソースコードを読んでいたらdeep mergeしてたので面白いな〜と思い調べるに至りました。
Elixir標準のMap.merge/2
Elixir言語には標準のMap.merge/2
があります。便利です。
%{a: 1} |> Map.merge(%{b: 2}) |> Map.merge(%{c: 3})
# %{a: 1, b: 2, c: 3}
しかしながら、Mapの構造が入れ子になっている場合、再帰的に統合することはしてくれません。
entry1 = %{"a" => %{"b" => %{"c1" => "hello", "c2" => "world" }}}
entry2 = %{"a" => %{"b" => %{"c1" => "コンニチハ", "c2" => "セカイ" }}}
entry3 = %{"a" => %{"b" => %{"c3" => "elixir" }}}
entry1 |> Map.merge(entry2) |> Map.merge(entry3)
# %{"a" => %{"b" => %{"c3" => "elixir"}}}
欲しい結果が以下のようなものである場合、悩まされるかもしれません。
%{
"a" => %{
"b" => %{"c1" => "コンニチハ", "c2" => "セカイ", "c3" => "elixir"}
}
}
そこで登場するのがdeep_merge
です。
やり方はいくつか考えられます。
deep_merge
(A)
今回の調査のきっかけとなった実装です。
defmodule MapUtils1 do
def deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
Map.merge(map1, map2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
end
def deep_merge(not_map1, not_map2), do: not_map2
end
entry1 |> MapUtils1.deep_merge(entry2) |> MapUtils1.deep_merge(entry3)
deep_merge
(B)
ネットで調べて出てきたのがこれです。
defmodule MapUtils2 do
def deep_merge(left, right) do
Map.merge(left, right, &deep_resolve/3)
end
defp deep_resolve(_key, left = %{}, right = %{}) do
deep_merge(left, right)
end
defp deep_resolve(_key, _left, right) do
right
end
end
entry1 |> MapUtils2.deep_merge(entry2) |> MapUtils2.deep_merge(entry3)
deep_merge
(C)
- もっと簡潔に書けないかと思い、ボクノカンガエタサイキョウの
deep_merge
を書きました。 - Frankさんの実装が一番簡潔といえばそうなのですが、関数を一個にまとめてみたかったですし、あと個人的に無名関数でのパターンマッチングの見た目がかっこいいと思っているんです。
defmodule MapUtils3 do
def deep_merge(left, right) do
Map.merge(left, right, fn
_key, l = %{}, r = %{} -> deep_merge(l, r)
_key, _l, r -> r
end)
end
end
entry1 |> MapUtils3.deep_merge(entry2) |> MapUtils3.deep_merge(entry3)
deep_merge
(Keyword
型)
-
Config
モジュールの内部にKeyword
型の設定データをdeep_merge
する__merge__/2
という関数がありました。 -
deep_merge
をKeyword
型に対して実施したい場合に参考になるかもしれません。
deep_merge
(Keyword
型とMap
型両方)
- 必要であれば
Map
型とKeyword
型の両方に対応することもできそうです。
defmodule MapUtils5 do
def deep_merge(map1, map2) when is_map(map1) and is_map(map2) do
Map.merge(map1, map2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
end
def deep_merge(kw1, kw2) when is_list(kw1) and is_list(kw2) do
Keyword.merge(kw1, kw2, fn _key, value1, value2 -> deep_merge(value1, value2) end)
end
def deep_merge(_not_map1, not_map2), do: not_map2
end
# Map
entry1 = %{"a" => %{"b" => %{"c1" => "hello", "c2" => "world" }}}
entry2 = %{"a" => %{"b" => %{"c1" => "コンニチハ", "c2" => "セカイ" }}}
entry3 = %{"a" => %{"b" => %{"c3" => "elixir" }}}
entry1 |> MapUtils5.deep_merge(entry2) |> MapUtils5.deep_merge(entry3)
# Keyword
entry1 = [a: [b: [c1: "hello", c2: "world" ]]]
entry2 = [a: [b: [c1: "コンニチハ", c2: "セカイ" ]]]
entry3 = [a: [b: [c3: "elixir" ]]]
entry1 |> MapUtils5.deep_merge(entry2) |> MapUtils5.deep_merge(entry3)
deep_merge
(Hexパッケージ)
-
deep_merge
関連の色んな機能がHexパッケージにされています。 - ほとんどの場合、上述の知識を持って自分で関数を書いた方が早いと思いますが、シンプルな実装では対処できない特殊な場合に活躍するようです。
iex
Mix.install([:deep_merge])
entry1 |> DeepMerge.deep_merge(entry2) |> DeepMerge.deep_merge(entry3)
さいごに
Elixirを楽しみましょう!
各コミュニティの詳細は、「Elixirコミュニティの歩き方 -国内オンライン編-」をご覧ください
各種Elixirイベントの開催予定は、「Elixirイベントカレンダー」から確認/参加できます